【Vue2】3-使用Vue脚手架

目录

初始化脚手架

说明

具体步骤

模板项目的结构

关于不同版本的Vue

vue.config.js配置文件

ref属性

配置项props

mixin(混入)

插件

scoped样式

总结TodoList案例

webStorage(浏览器本地存储)

TodoList本地存储

组件的自定义事件

全局事件总线(GlobalEventBus)

TodoList事件总线

消息订阅与发布(pubsub)

TodoList_pubsub

nextTick

Vue封装的过渡与动画

TodoList_动画


初始化脚手架

说明

Vue脚手架是Vue官方提供的标准化开发工具(开发平台)

文档:https://cli.vuejs.org/zh/

具体步骤

1.全局安装@vue/cli

npm install -g @vue/cli

2.切换到要创建项目的目录,然后使用命令创建项目

vue create xxxx

3.启动项目

npm run serve

备注:

Vue脚手架隐藏了所有webpack相关的配置,若想查看具体的webpack配置,执行:

vue inspect > output.js

模板项目的结构

分析脚手架:

main.js:

/**
 * 该文件是整个项目的入口文件
 */

//引入Vue
import Vue from 'vue'
//引入App组件,它是所有组件的父组件
import App from './App.vue'

//关闭Vue的生产提示
Vue.config.productionTip = false


//创建Vue实例对象vm
new Vue({
  el:'#app',
  //将App组件放入容器中
  render: h => h(App),
})

App.vue:

<template>
    <div>
        <img src="./assets/logo.png" alt="logo">
        <MySchool></MySchool>
        <MyStudent></MyStudent>
    </div>
</template>

<script>
    //引入组件
    import MySchool from './components/MySchool';
    import MyStudent from './components/MyStudent';

    export default {
        name:'App',
        components:{
            MySchool,
            MyStudent
        }
    }
</script>

<style>
    
</style>

MySchool.vue:

<template>
    <!-- 组件的结构 -->
    <div class="demo">
        <h2>学校名称:{{SchoolName}}</h2>
        <h2>学校地址:{{address}}</h2>
        <button @click="showName">点我提示学校名</button>
    </div>
</template>

<script>
    //组件交互相关的代码(数据、方法等等)
    export default{
        name:'MySchool',
        data(){
            return{
                SchoolName:'尚硅谷1',
                address:'北京'
            }
        },
        methods: {
            showName(){
                alert(this.SchoolName)
            }
        }
    }

</script>

<style>
    /* 组件的样式 */
    .demo{
        background-color: orange;
    }
</style>

MyStudent.vue:

<template>
    <!-- 组件的结构 -->
    <div>
        <h2>学生姓名:{{StudentName}}</h2>
        <h2>学校年龄:{{age}}</h2>
    </div>
</template>

<script>
    //组件交互相关的代码(数据、方法等等)
    export default{
        name:'MyStudent',
        data(){
            return{
                StudentName:'张三',
                age:18
            }
        }
    }

</script>



关于不同版本的Vue

vue.js与vue.runtime.xxx.js的区别:

(1)vue.js是完整版的Vue,包含:核心功能+模板解析器

(2)vue.runtime.xxx.js是运行版的Vue,只包含:核心功能,没有模板解析器

因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收

到的createElement函数去指定具体内容

vue.config.js配置文件

使用vue inspect > output.js可以查看到Vue脚手架的默认配置

使用vue.config.js可以对脚手架进行个性化定制,详情见:https://cli.vuejs.org/zh

ref属性

  • 被用来给元素或子组件注册引用信息(id的替代者)
  • 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
  • 使用方式:

        打标识:<h1 ref="xxx">......</h1>或<School ref="xxx"></School>

        获取:this.$refs.xxx

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false

//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div>
        <h1 v-text="msg" ref="title"></h1>
        <button ref="btn" @click="showDOM">点我输出上方的DOM元素</button>
        <School ref="sch"/>
    </div>
</template>
<script>
    //引入School组件
    import School from './components/School'

    export default {
        name:'App',
        components:{
            School
        },
        data() {
            return {
                msg:'欢迎学习Vue'
            }
        },
        methods:{
            showDOM(){
                console.log(this.$refs.title) //真实DOM元素
                console.log(this.$refs.btn) //真实DOM元素
                console.log(this.$refs.sch) //School组件的实例对象(vc)
            }
        }
    }
</script>

School.vue:

<template>
    <div class="school">
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
    </div>
</template>
<script>
export default {
    name:'School',
    data(){
        return{
            name:'尚硅谷1',
            address:'北京'
        }
    }
}
</script>
<style>
    .school{
        background-color: gray;
    }
</style>

配置项props

 功能:让组件接收外部传过来的数据

1.传递数据:

<Demo name="xxx"/>

2.接收数据:

(1)第一种方式(只接收):

props:['name']

(2)第二种方式(限制类型):

props:{
    name:String
}

(3)第三种方式(限制类型、限制必要性、指定默认值):

props:{
    name:{
        type:String, //类型
        required:true, //必要性
        default:'张三' //默认值
    }
}

备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业

务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false

//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div>
        <Student name="李四" sex="女" :age="18"/>
    </div>
</template>
<script>
    //引入Student组件
    import Student from './components/Student.vue';

    export default {
        name:'App',
        components:{
            Student
        }
    }
</script>

Student.vue:

<template>
    <div>
        <h1>{{msg}}</h1>
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <h2>学生年龄:{{myAge+1}}</h2>
        <button @click="updateAge">尝试修改收到的年龄</button>
    </div>
</template>
<script>
export default {
    name:'Student',
    data(){
        return{
            msg:'我是一个尚硅谷的学生',
            myAge:this.age
        }
    },
    methods:{
        updateAge(){
            this.myAge++
        }
    },
    //简单声明接收
    props:['name','sex','age'] 

    //接收的同时对数据进行类型限制
    /* props:{
        name:String,
        age:Number,
        sex:String
    } */

    //接收的同时对数据进行类型限制+默认值的指定+必要性的限制
    /* props:{
        name:{
            type:String, //name的类型
            required:true //name是必要的
        },
        age:{
            type:Number,
            default:99 //默认值
        },
        sex:{
            type:String,
            required:true
        }
    } */

}
</script>

mixin(混入)

功能:可以把多个组件共用的配置提取成一个混入对象

使用方式:

  • 第一步:定义混入
{
    data(){

    },
    methods:{
        
    }
    ......
}
  • 第二步:使用混入

(1)全局混入:Vue.mixin(xxx)

(2)局部混入:mixins:['xxx']

mixin.js:

export const hunhe = {
    methods:{
        showName(){
            alert(this.name)
        }
    },
    mounted() {
        console.log('你好')
    }
}

export const hunhe2 = {
    data() {
        return {
            x:100,
            y:200
        }
    }
}

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";
//引入混入
import { hunhe,hunhe2 } from "./mixin";

Vue.config.productionTip = false

Vue.mixin(hunhe)
Vue.mixin(hunhe2)

//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div>
        <School/>
        <hr>
        <Student/>
    </div>
</template>
<script>
    //引入School组件
    import School from './components/School.vue';
    import Student from './components/Student.vue';

    export default {
        name:'App',
        components:{
            School,
            Student
        }
    }
</script>

School.vue:

<template>
    <div>
        <h2 @click="showName">学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
    </div>
</template>

<script>
// import { hunhe,hunhe2 } from '@/mixin';

export default {
    name:'School',
    data(){
        return{
            name:'尚硅谷',
            address:'北京'
        }
    },
    // mixins:[hunhe,hunhe2],
    mounted(){
        console.log('你好!!!')
    }
}
</script>

Student.vue:

<template>
    <div>
        <h2 @click="showName">学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
    </div>
</template>

<script>
// import { hunhe } from '@/mixin';
export default {
    name:'Student',
    data(){
        return{
            name:'张三',
            sex:'男'
        }
    },
    // mixins:[hunhe]
}
</script>

插件

功能:用于增强Vue

本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传

递的数据

定义插件:

对象.install = function(Vue,options){
      //1.添加全局过滤器
      Vue.filter(...)
      //2.添加全局指令
      Vue.directive(...)
      //3.配置全局混入
      Vue.mixin(...)
      //4.添加实例方法
      Vue.prototype.$myMethod = function(){...}
      Vue.prototype.$myProperty = xxx
}

使用插件:Vue.use()

plugins.js:

export default {
    install(Vue){
        //全局过滤器
        Vue.filter('mySlice',function(value){
            return value.slice(0,4)
        })


        //全局指令(同过滤器)
        Vue.directive('fbind',{
            //指令与元素成功绑定时(一上来)
            bind(element,binding){
                        element.value = binding.value
                    },
                    //指令所在元素被插入页面时
                    inserted(element,binding){
                        element.focus()
                    },
                    //指令所在的模板被重新解析时
                    update(element,binding){
                        element.value = binding.value
                    }
        })

        //定义混入
        Vue.mixin({
            data(){
                return{
                    x:100,
                    y:200
                }
            }
        })

        //给Vue原型上添加一个方法(vm和vc都能使用)
        Vue.prototype.hello = ()=>(alert('你好'))
    }
}

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";
//引入插件
import plugins from "./plugins";

Vue.config.productionTip = false

//应用插件
Vue.use(plugins)

//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div>
        <School/>
        <hr>
        <Student/>
    </div>
</template>
<script>
    //引入School组件
    import School from './components/School.vue';
    import Student from './components/Student.vue';

    export default {
        name:'App',
        components:{
            School,
            Student
        }
    }
</script>

School.vue:

<template>
    <div>
        <h2>学校名称:{{name | mySlice}}</h2>
        <h2>学校地址:{{address}}</h2>
        <button @click="test">点我测试hello方法</button>
    </div>
</template>

<script>
export default {
    name:'School',
    data(){
        return{
            name:'尚硅谷atguigu',
            address:'北京'
        }
    },
    methods:{
        test(){
            this.hello()
        }
    }
}
</script>

Student.vue:

<template>
    <div>
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <input type="text" v-fbind:value="name">
    </div>
</template>

<script>

export default {
    name:'Student',
    data(){
        return{
            name:'张三',
            sex:'男'
        }
    }
}
</script>

scoped样式

作用:让样式在局部生效,防止冲突

写法:<style scoped>

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div>
        <School/>
        <Student/>
    </div>
</template>
<script>
    //引入School组件
    import School from './components/School.vue';
    import Student from './components/Student.vue';

    export default {
        name:'App',
        components:{
            School,
            Student
        }
    }
</script>

School.vue:

<template>
    <div class="demo">
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
    </div>
</template>

<script>
export default {
    name:'School',
    data(){
        return{
            name:'尚硅谷atguigu',
            address:'北京'
        }
    }
}
</script>

<style scoped>
  .demo{
    background-color: skyblue
  }
</style>

Student.vue:

<template>
    <div class="demo">
        <h2>学生姓名:{{name}}</h2>
        <h2 class="atguigu">学生性别:{{sex}}</h2>
    </div>
</template>

<script>

export default {
    name:'Student',
    data(){
        return{
            name:'张三',
            sex:'男'
        }
    },

}
</script>

<style lang="less" scoped>
  .demo{
    background-color: orange;
    .atguigu{
        font-size: 40px;
    }
  }
</style>

总结TodoList案例

组件化编码流程:

1.拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突

2.实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

    (1)一个组件在用:放在组件自身即可

    (2)一些组件在用:放在他们共同的父组件上(状态提升)

3.实现交互:从绑定事件开始

props适用于:

1.父组件 ===> 子组件 通信

2.子组件 ===> 父组件 通信(要求父先给子一个函数)

使用v-mode时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的

props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div id="root">
        <div class="todo-container">
          <div class="todo-wrap">
            <MyHeader :addTodo="addTodo"/>
            <MyList :todos="todos" :checkedTodo="checkedTodo" :deleteTodo="deleteTodo"/>
            <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
          </div>
        </div>
      </div>
</template>
<script>
    import MyHeader from './components/MyHeader.vue';
    import MyList from './components/MyList.vue';
    import MyFooter from './components/MyFooter.vue';
    
    export default {
        name:'App',
        components:{
            MyHeader,
            MyList,
            MyFooter
            
        },
        data(){
            return{
                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                todos:[
                    {id:'001',title:'吃饭',done:true},
                    {id:'002',title:'睡觉',done:false},
                    {id:'003',title:'开车',done:true}
                ]
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkedTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter((todo)=>{
                    return todo.id !== id
                })
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.done
                })
            }
        }
    }
</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>

MyFooter.vue:

<template>
    <div class="todo-footer" v-show="total">
        <label>
          <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
          <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos','checkAllTodo','clearAllTodo'],
        computed:{
            total(){
                return this.todos.length
            },
            doneTotal(){
                //第一种方式
                let i = 0
                this.todos.forEach((todo) => {
                    if(todo.done) i++
                })
                return i

                //第二种方式
                // return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
            },
            isAll(){
                return this.doneTotal === this.total && this.total > 0
            }
        },
        methods: {
            checkAll(e){
                this.checkAllTodo(e.target.checked)
            },
            clearAll(){
                this.clearAllTodo()
            }
        }
    }
</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>

MyHeader.vue:

<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
    </div>
</template>

<script>
    import { nanoid } from 'nanoid';

    export default {
        name:'MyHeader',
        props:['addTodo'],
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(),title:this.title,done:false}
                //通知App组件添加一个todo对象
                this.addTodo(todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</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>

MyItem.vue:

<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 如下代码也能实现功能,但不推荐,违背原则,修改了props -->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    export default {
        name:'MyItem',
        //声明接收todo对象
        props:['todo','checkedTodo','deleteTodo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id){
                //通知App组件将对应的todo对象的done值取反
                this.checkedTodo(id)
            },
            //删除
            handleDelete(id){
                if(confirm('确定删除吗?')){
                    this.deleteTodo(id)
                }
            }
        },
        
    }
</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;
    }

    li:hover{
        background-color: #ddd;
    }

    li:hover button{
        display: block;
    }
</style>

MyList.vue:

<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" :todo="todoObj" 
            :checkedTodo="checkedTodo"
            :deleteTodo="deleteTodo"
        />
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue';
    
    export default {
        name:'MyList',
        components:{
            MyItem
        },
        props:['todos','checkedTodo','deleteTodo']
    }
</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>

webStorage(浏览器本地存储)

存储内容大小一般支持5MB左右(不同的浏览器可能不一样)

浏览器端通过Window.sessionStorage和Window.localStorage属性来实现本地存储机制

相关API:

  • xxxxxStorage.setItem('key','value'); //该方法接收一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值
  • xxxxxStorage.getItem('person'); //该方法接收一个键名作为参数,返回键名对应的值
  • xxxxxStorage.removeItem('key'); //该方法接收一个键名作为参数,并把该键名从存储中删除
  • xxxxxStorage.clear(); //该方法会清空存储中的所有数据

备注:

  • sessionStorage存储的内容会随着浏览器窗口关闭而消失
  • localStorage存储的内容需要手动消除才会消失
  • xxxxxStorage.getItem(xxx)如果xxx对应的value获取不到,那么getItem的返回值是null
  • JSON.parse(null)的结果依然是null

localStorage.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>localStorage</title>
</head>
<body>
    <h2>localStorage</h2>
    <button onclick="saveData()">点我保存一个数据</button>
    <button onclick="readData()">点我读取一个数据</button>
    <button onclick="deleteData()">点我删除一个数据</button>
    <button onclick="deleteAllData()">点我清空一个数据</button>

    <script type="text/javascript">
        let p = {name:'张三',age:18}

        function saveData(){
            localStorage.setItem('msg','hello!')
            localStorage.setItem('msg2',666)
            localStorage.setItem('person',JSON.stringify(p))
        }

        function readData(){
            console.log(localStorage.getItem('msg'))
            console.log(localStorage.getItem('msg2'))
            const result  =localStorage.getItem('person')
            console.log(JSON.parse(result))
        }

        function deleteData(){
            localStorage.removeItem('msg')
        }


        function deleteAllData(){
            localStorage.clear()
        }
    </script>
</body>
</html>

sessionStorage.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>sessionStorage</title>
</head>
<body>
    <h2>sessionStorage</h2>
    <button onclick="saveData()">点我保存一个数据</button>
    <button onclick="readData()">点我读取一个数据</button>
    <button onclick="deleteData()">点我删除一个数据</button>
    <button onclick="deleteAllData()">点我清空一个数据</button>

    <script type="text/javascript">
        let p = {name:'张三',age:18}

        function saveData(){
            sessionStorage.setItem('msg','hello!')
            sessionStorage.setItem('msg2',666)
            sessionStorage.setItem('person',JSON.stringify(p))
        }

        function readData(){
            console.log(sessionStorage.getItem('msg'))
            console.log(sessionStorage.getItem('msg2'))
            const result  =sessionStorage.getItem('person')
            console.log(JSON.parse(result))
        }

        function deleteData(){
            sessionStorage.removeItem('msg')
        }


        function deleteAllData(){
            sessionStorage.clear()
        }
    </script>
</body>
</html>

TodoList本地存储

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div id="root">
        <div class="todo-container">
          <div class="todo-wrap">
            <MyHeader :addTodo="addTodo"/>
            <MyList :todos="todos" :checkedTodo="checkedTodo" :deleteTodo="deleteTodo"/>
            <MyFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
          </div>
        </div>
      </div>
</template>
<script>
    import MyHeader from './components/MyHeader.vue';
    import MyList from './components/MyList.vue';
    import MyFooter from './components/MyFooter.vue';
    
    export default {
        name:'App',
        components:{
            MyHeader,
            MyList,
            MyFooter
            
        },
        data(){
            return{
                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkedTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter((todo)=>{
                    return todo.id !== id
                })
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.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>

MyFooter.vue:

<template>
    <div class="todo-footer" v-show="total">
        <label>
          <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
          <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos','checkAllTodo','clearAllTodo'],
        computed:{
            //总数
            total(){
                return this.todos.length
            },
            //已完成数
            doneTotal(){
                //第一种方式
                let i = 0
                this.todos.forEach((todo) => {
                    if(todo.done) i++
                })
                return i

                //第二种方式(使用reduce方法做条件统计)
                // return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
            },
            //控制全选框
            isAll(){
                return this.doneTotal === this.total && this.total > 0
            }
        },
        methods: {
            checkAll(e){
                this.checkAllTodo(e.target.checked)
            },
            clearAll(){
                this.clearAllTodo()
            }
        }
    }
</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>

MyHeader.vue:

<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
    </div>
</template>

<script>
    import { nanoid } from 'nanoid';

    export default {
        name:'MyHeader',
        props:['addTodo'],
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(),title:this.title,done:false}
                //通知App组件添加一个todo对象
                this.addTodo(todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</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>

MyItem.vue:

<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 如下代码也能实现功能,但不推荐,违背原则,修改了props -->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    export default {
        name:'MyItem',
        //声明接收todo对象
        props:['todo','checkedTodo','deleteTodo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id){
                //通知App组件将对应的todo对象的done值取反
                this.checkedTodo(id)
            },
            //删除
            handleDelete(id){
                if(confirm('确定删除吗?')){
                    this.deleteTodo(id)
                }
            }
        },
        
    }
</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;
    }

    li:hover{
        background-color: #ddd;
    }

    li:hover button{
        display: block;
    }
</style>

MyList.vue:

<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" :todo="todoObj" 
            :checkedTodo="checkedTodo"
            :deleteTodo="deleteTodo"
        />
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue';
    
    export default {
        name:'MyList',
        components:{
            MyItem
        },
        props:['todos','checkedTodo','deleteTodo']
    }
</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>

组件的自定义事件

一种组件间通信的方式,适用于:子组件 ===> 父组件

使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的

回调在A中)

绑定自定义事件:

第一种方式:在父组件中:

<Demo @atguigu="test"/>或<Demo v-on:atguigu="test"/>

第二种方式:

<Demo ref="demo"/>
    ......
    mounted(){
      this.$refs.xxx.$on('atguigu',this.test)
}

若想让自定义事件只能触发一次,可以使用once修饰符或$once方法

触发自定义事件:this.$emit('atguigu',数据)

解绑自定义事件:this.$off('atguigu')

组件上也可以绑定原生DOM事件,需要使用native修饰符

注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中,要

么用箭头函数,否则this指向会出问题

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    mounted() {
        setTimeout(()=>{
            this.$destroy()
        },3000)
    },
})

App.vue:

<template>
    <div class="app">
        <h1>{{msg}},学生姓名是:{{studentName}}</h1>
        <!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
        <School :getSchoolName="getSchoolName"/>
        <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法:使用@或v-on) -->
        <!-- <Student @atguigu="getStudentName" @demo="m1"/> -->
         <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法:使用ref) -->
        <Student ref="student" @click.native="show"/>
    </div>
</template>
<script>
    //引入School组件
    import School from './components/School.vue';
    import Student from './components/Student.vue';

    export default {
        name:'App',
        components:{
            School,
            Student
        },
        data() {
            return {
                msg:'你好',
                studentName:''
            }
        },
        methods:{
            getSchoolName(name){
                console.log('App收到了学校名:',name)
            },
            getStudentName(name){
                console.log('App收到了学生名:',name)
                this.studentName = name
            },
            m1(){
                console.log('demo事件被触发了')
            },
            show(){
                alert(123)
            }
        },
        mounted() {
            this.$refs.student.$on('atguigu',this.getStudentName) //绑定自定义事件
            // this.$refs.student.$once('atguigu',this.getStudentName) //绑定自定义事件(一次性)
        }
    }
</script>

<style scoped>
    .app{
        background-color: gray;
        padding: 5px
    }
</style>

School.vue:

<template>
    <div class="school">
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
        <button @click="sendSchoolName">把学校名给App</button>
    </div>
</template>

<script>
export default {
    name:'School',
    props:['getSchoolName'],
    data(){
        return{
            name:'尚硅谷',
            address:'北京'
        }
    },
    methods:{
        sendSchoolName(){
            this.getSchoolName(this.name)
        }
    }
}
</script>

<style scoped>
  .school{
    background-color: skyblue;
    padding: 5px;
    
  }
</style>

Student.vue:

<template>
    <div class="student">
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <h2>当前求和为:{{number}}</h2>
        <button @click="add">点我number++</button>
        <button @click="sendStudentName">把学生名给App</button>
        <button @click="unbind">解绑atguigu事件</button>
        <button @click="death">销毁当前Student组件的实例(vc)</button>
    </div>
</template>

<script>

export default {
    name:'Student',
    data(){
        return{
            name:'张三',
            sex:'男',
            number:0
        }
    },
    methods: {
        add(){
            console.log('add回调被调用了')
            this.number++
        },
        sendStudentName(){
            //触发Student组件实例上的aiguigu事件
            this.$emit('atguigu',this.name)
            // this.$emit('demo')
            this.$emit('click')
        },
        unbind(){
            this.$off('atguigu') //解绑一个自定义事件
            // this.$off(['atguigu','demo']) //解绑多个自定义事件
            // this.$off() //解绑所有的自定义事件
        },
        death(){
            this.$destroy() //销毁了当前Student组件的实例,销毁后所有Student实例的自定义事件全都不奏效
        }
    },

}
</script>

<style lang="less" scoped>
  .student{
    background-color: orange;
    padding: 5px;
    margin-top: 30px
  }
</style>

全局事件总线(GlobalEventBus)

一种组件间通信的方式,适用于任意组件间通信

安装全局事件总线:

new Vue({
    ......
    beforeCreate(){
      Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
    }
    ......
})

使用事件总线:

1.接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件本身

methods:{
    demo(data){
      ......
    }
}
......
mounted(){
    this.$bus.$on('xxxx',this.demo)
}

2.提供数据:this.$bus.$emit('xxxx',数据)

说明:最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    beforeCreate(){
        Vue.prototype.$bus = this //安装全局事件总线
    }
})

App.vue:

<template>
    <div class="app">
        <h1>{{msg}}</h1>
        <School/>
        <Student/>
    </div>
</template>
<script>
    //引入School组件
    import School from './components/School.vue';
    import Student from './components/Student.vue';

    export default {
        name:'App',
        components:{
            School,
            Student
        },
        data() {
            return {
                msg:'你好'
            }
        }
    }
</script>

<style scoped>
    .app{
        background-color: gray;
        padding: 5px
    }
</style>

School.vue:

<template>
    <div class="school">
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
    </div>
</template>

<script>
export default {
    name:'School',
    props:['getSchoolName'],
    data(){
        return{
            name:'尚硅谷',
            address:'北京'
        }
    },
    mounted(){
        console.log('School',this)
        this.$bus.$on('hello',(data)=>{
            console.log('我是School组件,收到了数据',data)
        })
    },
    beforeDestroy() {
        this.$bus.$off('hello')
    },
}
</script>

<style scoped>
  .school{
    background-color: skyblue;
    padding: 5px;
    
  }
</style>

Student.vue:

<template>
    <div class="student">
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <button @click="sendStudentName">把学生名给School组件</button>
    </div>
</template>

<script>

export default {
    name:'Student',
    data(){
        return{
            name:'张三',
            sex:'男'
        }
    },
    methods:{
        sendStudentName(){
            this.$bus.$emit('hello',this.name)
        }
    }
}
</script>

<style lang="less" scoped>
  .student{
    background-color: orange;
    padding: 5px;
    margin-top: 30px
  }
</style>

TodoList事件总线

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})

App.vue:

<template>
    <div id="root">
        <div class="todo-container">
          <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
          </div>
        </div>
      </div>
</template>
<script>
    import MyHeader from './components/MyHeader.vue';
    import MyList from './components/MyList.vue';
    import MyFooter from './components/MyFooter.vue';
    
    export default {
        name:'App',
        components:{
            MyHeader,
            MyList,
            MyFooter
            
        },
        data(){
            return{
                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkedTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                this.todos = this.todos.filter((todo)=>{
                    return todo.id !== id
                })
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.done
                })
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        },
        mounted() {
            this.$bus.$on('checkedTodo',this.checkedTodo)
            this.$bus.$on('deleteTodo',this.deleteTodo)
        },
        beforeDestroy(){
            this.$bus.$off('checkedTodo')
            this.$bus.$off('deleteTodo')
        }
    }
</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>

MyFooter.vue:

<template>
    <div class="todo-footer" v-show="total">
        <label>
          <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
          <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            //总数
            total(){
                return this.todos.length
            },
            //已完成数
            doneTotal(){
                //第一种方式
                let i = 0
                this.todos.forEach((todo) => {
                    if(todo.done) i++
                })
                return i

                //第二种方式(使用reduce方法做条件统计)
                // return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
            },
            //控制全选框
            isAll(){
                return this.doneTotal === this.total && this.total > 0
            }
        },
        methods: {
            checkAll(e){
                // this.checkAllTodo(e.target.checked)
                this.$emit('checkAllTodo',e.target.checked)
            },
            clearAll(){
                // this.clearAllTodo()
                this.$emit('clearAllTodo')
            }
        }
    }
</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>

MyHeader.vue:

<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
    </div>
</template>

<script>
    import { nanoid } from 'nanoid';

    export default {
        name:'MyHeader',
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(),title:this.title,done:false}
                //通知App组件添加一个todo对象
                this.$emit('addTodo',todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</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>

MyItem.vue:

<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 如下代码也能实现功能,但不推荐,违背原则,修改了props -->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    export default {
        name:'MyItem',
        //声明接收todo对象
        props:['todo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id){
                //通知App组件将对应的todo对象的done值取反
                // this.checkedTodo(id)
                this.$bus.$emit('checkedTodo',id)
            },
            //删除
            handleDelete(id){
                if(confirm('确定删除吗?')){
                    // this.deleteTodo(id)
                    this.$bus.$emit('deleteTodo',id)
                }
            }
        }
        
    }
</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;
    }

    li:hover{
        background-color: #ddd;
    }

    li:hover button{
        display: block;
    }
</style>

MyList.vue:

<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" :todo="todoObj" 
        />
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue';
    
    export default {
        name:'MyList',
        components:{
            MyItem
        },
        props:['todos']
    }
</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>

消息订阅与发布(pubsub)

一种组件间通信的方式,适用于任意组件间通信

 使用步骤:

1.安装pubsub:npm i pubsub-js

2.引入:import pubsub from 'pubsub-js'

3.接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身

methods:{
  demo(data){......}
}
......
mounted(){
  this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
}

4.提供数据:pubsub.publish('xxx',数据)

5.最好在beforeDestroy钩子中,用pubsub.unsubscribe(pid)去<span style="color:red">取消订阅</span>

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})

App.vue:

<template>
    <div class="app">
        <h1>{{msg}}</h1>
        <School/>
        <Student/>
    </div>
</template>
<script>
    //引入School组件
    import School from './components/School.vue';
    import Student from './components/Student.vue';

    export default {
        name:'App',
        components:{
            School,
            Student
        },
        data() {
            return {
                msg:'你好'
            }
        }
    }
</script>

<style scoped>
    .app{
        background-color: gray;
        padding: 5px
    }
</style>

School.vue:

<template>
    <div class="school">
        <h2>学校名称:{{name}}</h2>
        <h2>学校地址:{{address}}</h2>
    </div>
</template>

<script>
import pubsub from 'pubsub-js'
export default {
    name:'School',
    props:['getSchoolName'],
    data(){
        return{
            name:'尚硅谷',
            address:'北京'
        }
    },
    methods: {
        demo(msgName,data){
            console.log('有人发布了hello消息,hello消息的回调执行了',data,this)
        }
    },
    mounted(){
        // console.log('School',this)
        // this.$bus.$on('hello',(data)=>{
            // console.log('我是School组件,收到了数据',data)
        // })
        this.pubId = pubsub.subscribe('hello',this.demo)
    },
    beforeDestroy() {
        // this.$bus.$off('hello')
        pubsub.unsubscribe(this.pubId)
    },
}
</script>

<style scoped>
  .school{
    background-color: skyblue;
    padding: 5px;
    
  }
</style>

Student.vue:

<template>
    <div class="student">
        <h2>学生姓名:{{name}}</h2>
        <h2>学生性别:{{sex}}</h2>
        <button @click="sendStudentName">把学生名给School组件</button>
    </div>
</template>

<script>
import pubsub from 'pubsub-js'
export default {
    name:'Student',
    data(){
        return{
            name:'张三',
            sex:'男'
        }
    },
    methods:{
        sendStudentName(){
            // this.$bus.$emit('hello',this.name)
            pubsub.publish('hello',666)
        }
    }
}
</script>

<style lang="less" scoped>
  .student{
    background-color: orange;
    padding: 5px;
    margin-top: 30px
  }
</style>

TodoList_pubsub

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})

App.vue:

<template>
    <div id="root">
        <div class="todo-container">
          <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
          </div>
        </div>
      </div>
</template>
<script>
    import pubsub from 'pubsub-js'

    import MyHeader from './components/MyHeader.vue';
    import MyList from './components/MyList.vue';
    import MyFooter from './components/MyFooter.vue';
    
    export default {
        name:'App',
        components:{
            MyHeader,
            MyList,
            MyFooter
            
        },
        data(){
            return{
                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkedTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(_,id){
                this.todos = this.todos.filter((todo)=>{
                    return todo.id !== id
                })
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.done
                })
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        },
        mounted() {
            this.$bus.$on('checkedTodo',this.checkedTodo)
            this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
        },
        beforeDestroy(){
            this.$bus.$off('checkedTodo')
            pubsub.unsubscribe(this.pubId)
        }
    }
</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>

MyFooter.vue:

<template>
    <div class="todo-footer" v-show="total">
        <label>
          <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
          <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            //总数
            total(){
                return this.todos.length
            },
            //已完成数
            doneTotal(){
                //第一种方式
                let i = 0
                this.todos.forEach((todo) => {
                    if(todo.done) i++
                })
                return i

                //第二种方式(使用reduce方法做条件统计)
                // return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
            },
            //控制全选框
            isAll(){
                return this.doneTotal === this.total && this.total > 0
            }
        },
        methods: {
            checkAll(e){
                // this.checkAllTodo(e.target.checked)
                this.$emit('checkAllTodo',e.target.checked)
            },
            clearAll(){
                // this.clearAllTodo()
                this.$emit('clearAllTodo')
            }
        }
    }
</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>

MyHeader.vue:

<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
    </div>
</template>

<script>
    import { nanoid } from 'nanoid';

    export default {
        name:'MyHeader',
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(),title:this.title,done:false}
                //通知App组件添加一个todo对象
                this.$emit('addTodo',todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</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>

MyItem.vue:

<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 如下代码也能实现功能,但不推荐,违背原则,修改了props -->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span>{{todo.title}}</span>
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name:'MyItem',
        //声明接收todo对象
        props:['todo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id){
                //通知App组件将对应的todo对象的done值取反
                // this.checkedTodo(id)
                this.$bus.$emit('checkedTodo',id)
            },
            //删除
            handleDelete(id){
                if(confirm('确定删除吗?')){
                    // this.deleteTodo(id)
                    // this.$bus.$emit('deleteTodo',id)
                    pubsub.publish('deleteTodo',id)
                }
            }
        }
        
    }
</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;
    }

    li:hover{
        background-color: #ddd;
    }

    li:hover button{
        display: block;
    }
</style>

MyList.vue:

<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" :todo="todoObj" 
        />
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue';
    
    export default {
        name:'MyList',
        components:{
            MyItem
        },
        props:['todos']
    }
</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>

nextTick

语法:this.$nextTick(回调函数)

作用:在下一次DOM更新结束后执行其指定的回调

什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})

App.vue:

<template>
    <div id="root">
        <div class="todo-container">
          <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
          </div>
        </div>
      </div>
</template>
<script>
    import pubsub from 'pubsub-js'

    import MyHeader from './components/MyHeader.vue';
    import MyList from './components/MyList.vue';
    import MyFooter from './components/MyFooter.vue';
    
    export default {
        name:'App',
        components:{
            MyHeader,
            MyList,
            MyFooter
            
        },
        data(){
            return{
                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkedTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(_,id){
                this.todos = this.todos.filter((todo)=>{
                    return todo.id !== id
                })
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.done
                })
            },
            //更新一个todo
            updateTodo(id,title){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.title = title
                })
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        },
        mounted() {
            this.$bus.$on('checkedTodo',this.checkedTodo)
            this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
            this.$bus.$on('updateTodo',this.updateTodo)
        },
        beforeDestroy(){
            this.$bus.$off('checkedTodo')
            pubsub.unsubscribe(this.pubId)
            this.$bus.$off('updateTodo')
        }
    }
</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-edit {
        color: #fff;
        background-color: skyblue;
        border: 1px solid rgb(102, 159, 181);
        margin-right: 5px
    }

    .btn-edit:hover {
        color: #fff;
        background-color: rgb(102, 159, 181);
    }
    
    .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>

MyFooter.vue:

<template>
    <div class="todo-footer" v-show="total">
        <label>
          <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
          <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            //总数
            total(){
                return this.todos.length
            },
            //已完成数
            doneTotal(){
                //第一种方式
                let i = 0
                this.todos.forEach((todo) => {
                    if(todo.done) i++
                })
                return i

                //第二种方式(使用reduce方法做条件统计)
                // return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
            },
            //控制全选框
            isAll(){
                return this.doneTotal === this.total && this.total > 0
            }
        },
        methods: {
            checkAll(e){
                // this.checkAllTodo(e.target.checked)
                this.$emit('checkAllTodo',e.target.checked)
            },
            clearAll(){
                // this.clearAllTodo()
                this.$emit('clearAllTodo')
            }
        }
    }
</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>

MyHeader.vue:

<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
    </div>
</template>

<script>
    import { nanoid } from 'nanoid';

    export default {
        name:'MyHeader',
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(),title:this.title,done:false}
                //通知App组件添加一个todo对象
                this.$emit('addTodo',todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</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>

MyItem.vue:

<template>
    <li>
        <label>
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <!-- 如下代码也能实现功能,但不推荐,违背原则,修改了props -->
            <!-- <input type="checkbox" v-model="todo.done"/> -->
            <span v-show="!todo.isEdit">{{todo.title}}</span>
            <input 
            type="text" 
            v-show="todo.isEdit" 
            :value="todo.title" 
            @blur="handleBlur(todo,$event)"
            ref="inputTitle"
            >
        </label>
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
        <button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todo.isEdit">编辑</button>
    </li>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name:'MyItem',
        //声明接收todo对象
        props:['todo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id){
                //通知App组件将对应的todo对象的done值取反
                // this.checkedTodo(id)
                this.$bus.$emit('checkedTodo',id)
            },
            //删除
            handleDelete(id){
                if(confirm('确定删除吗?')){
                    // this.deleteTodo(id)
                    // this.$bus.$emit('deleteTodo',id)
                    pubsub.publish('deleteTodo',id)
                }
            },
            //编辑
            handleEdit(todo){
                if(todo.hasOwnProperty('isEdit')){
                    todo.isEdit = true
                }else{
                    this.$set(todo,'isEdit',true)
                }
                this.$nextTick(function(){
                    this.$refs.inputTitle.focus()
                })
            },
            //失去焦点回调(真正执行修改逻辑)
            handleBlur(todo,e){
                todo.isEdit = false
                if(!e.target.value.trim()) return alert('输入不能为空!')
                this.$bus.$emit('updateTodo',todo.id,e.target.value)
            }
        }
        
    }
</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;
    }

    li:hover{
        background-color: #ddd;
    }

    li:hover button{
        display: block;
    }
</style>

MyList.vue:

<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" :todo="todoObj" 
        />
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue';
    
    export default {
        name:'MyList',
        components:{
            MyItem
        },
        props:['todos']
    }
</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>

Vue封装的过渡与动画

作用:在插入、更新或移除DOM元素时,在合适的时候给元素添加样式类名

图示:

写法:

1.准备好样式:

  (1)元素进入的样式:

    v-enter:进入的起点

    v-enter-active:进入过程中

    v-enter-to:进入的终点

  (2)元素离开的样式:

    v-leave:离开的起点

    v-leave-active:离开过程中

    v-leave-to:离开的终点

2.使用<transition>包裹要过渡的元素,并配置name属性:

<transition name="hello">
  <h1 v-show="isShow">你好</h1>
</transition>

3.备注:若有多个元素需要过渡,则需要使用:<transition-group>,且每个元素都要指定key值

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})

App.vue:

<template>
    <div>
        <Demo/>
        <Demo2/>
        <Demo3/>      
    </div>
</template>
<script>
   import Demo from './components/Demo.vue'
   import Demo2 from './components/Demo2.vue'
   import Demo3 from './components/Demo3.vue'
    
    export default {
        name:'App',
        components:{
            Demo,
            Demo2,
            Demo3
        }
    }
</script>

Demo.vue:

<template>
    <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <transition name="hello" appear>
            <h1 v-show="isShow">你好</h1>
        </transition>
    </div>
</template>
<script>
    export default {
        name:'Demo',
        data() {
            return {
                isShow:true
            }
        }
    }
</script>

<style>
    h1{
        background-color: orange;
    }

    .hello-enter-active{
        animation: atguigu 0.5s linear;
    }

    .hello-leave-active{
        animation: atguigu 0.5s linear reverse;
    }

    @keyframes atguigu {
        from{
            transform: translateX(-100%);
        }
        to{
            transform: translateX(0px);
        }
    }
</style>

Demo2.vue:

<template>
    <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <transition-group name="hello" appear>
            <h1 v-show="!isShow" key="1">你好</h1>
            <h1 v-show="isShow" key="2">尚硅谷</h1>
        </transition-group>
    </div>
</template>
<script>
    export default {
        name:'Demo',
        data() {
            return {
                isShow:true
            }
        }
    }
</script>

<style scoped>
    h1{
        background-color: orange;
    }
    
    /* 进入的起点,离开的终点 */
    .hello-enter,.hello-leave-to{
        transform: translateX(-100%);
    }

    .hello-enter-active,.hello-leave-active{
        transform: 0.5s linear;
    }

    /* 进入的终点,离开的起点 */
    .hello-enter-to,.hello-leave{
        transform: translateX(0);
    }


    
</style>

Demo3.vue:

<template>
    <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <transition-group 
            name="animate__animated animate__bounce" 
            appear
            enter-active-class="animate__swing"
            leave-active-class="animate__backOutUp"
        >
            <h1 v-show="!isShow" key="1">你好</h1>
            <h1 v-show="isShow" key="2">尚硅谷</h1>
        </transition-group>
    </div>
</template>
<script>
    import 'animate.css'
    export default {
        name:'Demo',
        data() {
            return {
                isShow:true
            }
        }
    }
</script>

<style scoped>
    h1{
        background-color: orange;
    }
    


    
</style>

TodoList_动画

main.js:

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";

Vue.config.productionTip = false


//创建vm
new Vue({
    el:'#app',
    render: h => h(App),
    beforeCreate(){
        Vue.prototype.$bus = this
    }
})

App.vue:

<template>
    <div id="root">
        <div class="todo-container">
          <div class="todo-wrap">
            <MyHeader @addTodo="addTodo"/>
            <MyList :todos="todos"/>
            <MyFooter :todos="todos" @checkAllTodo="checkAllTodo" @clearAllTodo="clearAllTodo"/>
          </div>
        </div>
      </div>
</template>
<script>
    import pubsub from 'pubsub-js'

    import MyHeader from './components/MyHeader.vue';
    import MyList from './components/MyList.vue';
    import MyFooter from './components/MyFooter.vue';
    
    export default {
        name:'App',
        components:{
            MyHeader,
            MyList,
            MyFooter
            
        },
        data(){
            return{
                //由于todos是MyHeader组件和MyFooter组件都在使用,所以放在App中(状态提升)
                todos:JSON.parse(localStorage.getItem('todos')) || []
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkedTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(_,id){
                this.todos = this.todos.filter((todo)=>{
                    return todo.id !== id
                })
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已完成的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.done
                })
            },
            //更新一个todo
            updateTodo(id,title){
                this.todos.forEach((todo)=>{
                    if(todo.id == id) todo.title = title
                })
            }
        },
        watch:{
            todos:{
                deep:true,
                handler(value){
                    localStorage.setItem('todos',JSON.stringify(value))
                }
            }
        },
        mounted() {
            this.$bus.$on('checkedTodo',this.checkedTodo)
            this.pubId = pubsub.subscribe('deleteTodo',this.deleteTodo)
            this.$bus.$on('updateTodo',this.updateTodo)
        },
        beforeDestroy(){
            this.$bus.$off('checkedTodo')
            pubsub.unsubscribe(this.pubId)
            this.$bus.$off('updateTodo')
        }
    }
</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-edit {
        color: #fff;
        background-color: skyblue;
        border: 1px solid rgb(102, 159, 181);
        margin-right: 5px
    }

    .btn-edit:hover {
        color: #fff;
        background-color: rgb(102, 159, 181);
    }
    
    .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>

MyFooter.vue:

<template>
    <div class="todo-footer" v-show="total">
        <label>
          <input type="checkbox" :checked="isAll" @change="checkAll"/>
        </label>
        <span>
          <span>已完成{{doneTotal}}</span> / 全部{{total}}
        </span>
        <button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
    </div>
</template>

<script>
    export default {
        name:'MyFooter',
        props:['todos'],
        computed:{
            //总数
            total(){
                return this.todos.length
            },
            //已完成数
            doneTotal(){
                //第一种方式
                let i = 0
                this.todos.forEach((todo) => {
                    if(todo.done) i++
                })
                return i

                //第二种方式(使用reduce方法做条件统计)
                // return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
            },
            //控制全选框
            isAll(){
                return this.doneTotal === this.total && this.total > 0
            }
        },
        methods: {
            checkAll(e){
                // this.checkAllTodo(e.target.checked)
                this.$emit('checkAllTodo',e.target.checked)
            },
            clearAll(){
                // this.clearAllTodo()
                this.$emit('clearAllTodo')
            }
        }
    }
</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>

MyHeader.vue:

<template>
    <div class="todo-header">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
    </div>
</template>

<script>
    import { nanoid } from 'nanoid';

    export default {
        name:'MyHeader',
        data() {
            return {
                title:''
            }
        },
        methods:{
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(),title:this.title,done:false}
                //通知App组件添加一个todo对象
                this.$emit('addTodo',todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</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>

MyItem.vue:

<template>
    <transition name="todo" appear>
        <li>
            <label>
                <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
                <!-- 如下代码也能实现功能,但不推荐,违背原则,修改了props -->
                <!-- <input type="checkbox" v-model="todo.done"/> -->
                <span v-show="!todo.isEdit">{{todo.title}}</span>
                <input 
                type="text" 
                v-show="todo.isEdit" 
                :value="todo.title" 
                @blur="handleBlur(todo,$event)"
                ref="inputTitle"
                >
            </label>
            <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
            <button class="btn btn-edit" @click="handleEdit(todo)" v-show="!todo.isEdit">编辑</button>
        </li>
    </transition>
</template>

<script>
    import pubsub from 'pubsub-js'
    export default {
        name:'MyItem',
        //声明接收todo对象
        props:['todo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id){
                //通知App组件将对应的todo对象的done值取反
                // this.checkedTodo(id)
                this.$bus.$emit('checkedTodo',id)
            },
            //删除
            handleDelete(id){
                if(confirm('确定删除吗?')){
                    // this.deleteTodo(id)
                    // this.$bus.$emit('deleteTodo',id)
                    pubsub.publish('deleteTodo',id)
                }
            },
            //编辑
            handleEdit(todo){
                if(todo.hasOwnProperty('isEdit')){
                    todo.isEdit = true
                }else{
                    this.$set(todo,'isEdit',true)
                }
                this.$nextTick(function(){
                    this.$refs.inputTitle.focus()
                })
            },
            //失去焦点回调(真正执行修改逻辑)
            handleBlur(todo,e){
                todo.isEdit = false
                if(!e.target.value.trim()) return alert('输入不能为空!')
                this.$bus.$emit('updateTodo',todo.id,e.target.value)
            }
        }
        
    }
</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;
    }

    li:hover{
        background-color: #ddd;
    }

    li:hover button{
        display: block;
    }

    .todo-enter-active{
        animation: atguigu 0.5s linear;
    }

    .todo-leave-active{
        animation: atguigu 0.5s linear reverse;
    }

    @keyframes atguigu {
        from{
            transform: translateX(100%);
        }
        to{
            transform: translateX(0px);
        }
    }
</style>

MyList.vue:

<template>
    <ul class="todo-main">
        <MyItem 
            v-for="todoObj in todos" 
            :key="todoObj.id" :todo="todoObj" 
        />
    </ul>
</template>

<script>
    import MyItem from './MyItem.vue';
    
    export default {
        name:'MyList',
        components:{
            MyItem
        },
        props:['todos']
    }
</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>

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值