我是靠谱客的博主 糊涂水池,最近开发中收集的这篇文章主要介绍【15】Vue:03-组件、组件注册、Vue调试工具、Vue组件之间传值、组件插槽、购物车案例、todos案例、,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

    • day03
      • 组件
      • 组件注册
        • 全局注册
          • 组件基础用
          • 组件注意事项
        • 局部注册
      • Vue 调试工具
      • Vue组件之间传值
        • 父组件向子组件传值
        • 子组件向父组件传值
        • 兄弟之间的传递
      • 组件插槽
        • 匿名插槽
        • 具名插槽
        • 作用域插槽
      • 购物车案例
        • 1. 实现组件化布局
        • 2、实现 标题和结算功能组件
        • 3. 实现列表组件删除功能
        • 4. 实现组件更新数据功能 上
        • 5. 实现组件更新数据功能 下
        • 6. 源代码
      • 每日作业-Vue第03天
        • todos案例-组件化抽离
        • 题目描述
        • 训练目标
        • 训练提示
        • 操作步骤
        • 2 抽离头部组件
        • 3 抽离数据展示组件
        • 4 抽离fotter 部分
      • 每日作业-Vue第03天参考答案
        • todos案例-组件化抽离
        • 1 提供的数据和 HTML结构
        • 2 抽离头部组件
        • 3 抽离数据展示组件
        • 4 抽离fotter 部分
        • 5 源代码

day03

组件

  • 组件 (Component) 是 Vue.js 最强大的功能之一
  • 组件可以扩展 HTML 元素,封装可重用的代

组件注册

全局注册

  • Vue.component(‘组件名称’, { }) 第1个参数是标签名称,第2个参数是一个选项对象
  • 全局组件注册后,任何vue实例都可以用
组件基础用
<div id="example">
  <!-- 2、 组件使用 组件名称 是以HTML标签的形式使用  -->  
  <my-component></my-component>
</div>
<script>
    //   注册组件 
    // 1、 my-component 就是组件中自定义的标签名
	Vue.component('my-component', {
      template: '<div>A custom component!</div>'
    })

    // 创建根实例
    new Vue({
      el: '#example'
    })

</script>
<body>
  <div id="app">
    <button-counter></button-counter>
    <button-counter></button-counter>
    <button-counter></button-counter>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      组件注册
    */
    Vue.component('button-counter', {
      data: function(){
        return {
          count: 0
        }
      },
      template: '<button @click="handle">点击了{{count}}次</button>',
      methods: {
        handle: function(){
          this.count += 2;
        }
      }
    })
    var vm = new Vue({
      el: '#app',
      data: {
        
      }
    });
  </script>
</body>
组件注意事项
  • 组件参数的data值必须是函数同时这个函数要求返回一个对象
  • 组件模板必须是单个根元素
  • 组件模板的内容可以是模板字符串
  <div id="app">
     <!-- 
		4、  组件可以重复使用多次 
	      因为data中返回的是一个对象所以每个组件中的数据是私有的
		  即每个实例可以维护一份被返回对象的独立的拷贝   
	--> 
    <button-counter></button-counter>
    <button-counter></button-counter>
    <button-counter></button-counter>
      <!-- 8、必须使用短横线的方式使用组件 -->
     <hello-world></hello-world>
  </div>

<script type="text/javascript">
	//5  如果使用驼峰式命名组件,那么在使用组件的时候,只能在字符串模板中用驼峰的方式使用组件,
    // 7、但是在普通的标签模板中,必须使用短横线的方式使用组件
     Vue.component('HelloWorld', {
      data: function(){
        return {
          msg: 'HelloWorld'
        }
      },
      template: '<div>{{msg}}</div>'
    });
    
    
    
    Vue.component('button-counter', {
      // 1、组件参数的data值必须是函数 
      // 同时这个函数要求返回一个对象  
      data: function(){
        return {
          count: 0
        }
      },
      //  2、组件模板必须是单个根元素
      //  3、组件模板的内容可以是模板字符串  
      template: `
        <div>
          <button @click="handle">点击了{{count}}次</button>
          <button>测试123</button>
			#  6 在字符串模板中可以使用驼峰的方式使用组件	
		   <HelloWorld></HelloWorld>
        </div>
      `,
      methods: {
        handle: function(){
          this.count += 2;
        }
      }
    })
    var vm = new Vue({
      el: '#app',
      data: {
        
      }
    });
  </script>
<body>
  <div id="app">
    <button-counter></button-counter>
    <button-counter></button-counter>
    <button-counter></button-counter>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      组件注册注意事项
      1、组件参数的data值必须是函数
      2、组件模板必须是单个跟元素
      3、组件模板的内容可以是模板字符串

    */
    // Vue.component('button-counter', {
    //   data: function(){
    //     return {
    //       count: 0
    //     }
    //   },
    //   template: '<div><button @click="handle">点击了{{count}}次</button><button>测试</button></div>',
    //   methods: {
    //     handle: function(){
    //       this.count += 2;
    //     }
    //   }
    // })
    // -----------------------------------
    Vue.component('button-counter', {
      data: function(){
        return {
          count: 0
        }
      },
      template: `
        <div>
          <button @click="handle">点击了{{count}}次</button>
          <button>测试123</button>
        </div>
      `,
      methods: {
        handle: function(){
          this.count += 2;
        }
      }
    })
    var vm = new Vue({
      el: '#app',
      data: {
        
      }
    });
  </script>
</body>

局部注册

  • 只能在当前注册它的vue实例中使用
  <div id="app">
      <my-component></my-component>
  </div>


<script>
    // 定义组件的模板
    var Child = {
      template: '<div>A custom component!</div>'
    }
    new Vue({
      //局部注册组件  
      components: {
        // <my-component> 将只在父模板可用  一定要在实例上注册了才能在html文件中使用
        'my-component': Child
      }
    })
 </script>
<body>
  <div id="app">
    <hello-world></hello-world>
    <hello-tom></hello-tom>
    <hello-jerry></hello-jerry>
    <test-com></test-com>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      局部组件注册
      局部组件只能在注册他的父组件中使用
    */
    Vue.component('test-com',{
      template: '<div>Test<hello-world></hello-world></div>'
    });
    var HelloWorld = {
      data: function(){
        return {
          msg: 'HelloWorld'
        }
      },
      template: '<div>{{msg}}</div>'
    };
    var HelloTom = {
      data: function(){
        return {
          msg: 'HelloTom'
        }
      },
      template: '<div>{{msg}}</div>'
    };
    var HelloJerry = {
      data: function(){
        return {
          msg: 'HelloJerry'
        }
      },
      template: '<div>{{msg}}</div>'
    };
    var vm = new Vue({
      el: '#app',
      data: {
        
      },
      components: {
        'hello-world': HelloWorld,
        'hello-tom': HelloTom,
        'hello-jerry': HelloJerry
      }
    });
  </script>
</body>

Vue 调试工具

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <style type="text/css">
    .root {
      background-color: orange;
    }
    .second {
      background-color: lightgreen;
    }
    .third {
      background-color: lightblue;
    }
  </style>
</head>
<body>
  <div id="app" class="root">
    <div>{{root}}</div>
    <second-com></second-com>
    <second-com></second-com>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      Vue调试工具安装与基本使用
    */
    Vue.component('second-com',{
      data: function(){
        return {
          second: '二级组件'
        }
      },
      template: `<div class='second'>
        <div>{{second}}</div>
        <third-com></third-com>
        <third-com></third-com>
        <third-com></third-com>
      </div>`
    });
    Vue.component('third-com',{
      data: function(){
        return {
          third: '三级组件'
        }
      },
      template: '<div class="third"><div>{{third}}</div></div>'
    });
    
    var vm = new Vue({
      el: '#app',
      data: {
        root: '顶层组件'
      }
    });
  </script>
</body>
</html>

Vue组件之间传值

父组件向子组件传值

  • 父组件发送的形式是以属性的形式绑定值到子组件身上。
  • 然后子组件用属性props接收
  • 在props中使用驼峰形式,模板中需要使用短横线的形式字符串形式的模板中没有这个限制
  <div id="app">
    <div>{{pmsg}}</div>
     <!--1、menu-item  在 APP中嵌套着 故 menu-item   为  子组件      -->
     <!-- 给子组件传入一个静态的值 -->
    <menu-item title='来自父组件的值'></menu-item>
    <!-- 2、 需要动态的数据的时候 需要属性绑定的形式设置 此时 ptitle  来自父组件data 中的数据 . 
		  传的值可以是数字、对象、数组等等
	-->
    <menu-item :title='ptitle' content='hello'></menu-item>
  </div>

  <script type="text/javascript">
    Vue.component('menu-item', {
      // 3、 子组件用属性props接收父组件传递过来的数据  
      props: ['title', 'content'],
      data: function() {
        return {
          msg: '子组件本身的数据'
        }
      },
      template: '<div>{{msg + "----" + title + "-----" + content}}</div>'
    });
    var vm = new Vue({
      el: '#app',
      data: {
        pmsg: '父组件中内容',
        ptitle: '动态绑定属性'
      }
    });
  </script>
<body>
  <div id="app">
    <div>{{pmsg}}</div>
    <menu-item :menu-title='ptitle'></menu-item>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      父组件向子组件传值-props属性名规则
    */
    Vue.component('third-com', {
      props: ['testTile'],
      template: '<div>{{testTile}}</div>'
    });
    Vue.component('menu-item', {
      props: ['menuTitle'],
      template: '<div>{{menuTitle}}<third-com testTile="hello"></third-com></div>'
    });
    var vm = new Vue({
      el: '#app',
      data: {
        pmsg: '父组件中内容',
        ptitle: '动态绑定属性'
      }
    });
  </script>
</body>
<body>
  <div id="app">
    <div>{{pmsg}}</div>
    <menu-item :pstr='pstr' :pnum='12' pboo='true' :parr='parr' :pobj='pobj'></menu-item>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      父组件向子组件传值-props属性值类型
    */
    
    Vue.component('menu-item', {
      props: ['pstr','pnum','pboo','parr','pobj'],
      template: `
        <div>
          <div>{{pstr}}</div>
          <div>{{12 + pnum}}</div>
          <div>{{typeof pboo}}</div>
          <ul>
            <li :key='index' v-for='(item,index) in parr'>{{item}}</li>
          </ul>
            <span>{{pobj.name}}</span>
            <span>{{pobj.age}}</span>
          </div>
        </div>
      `
    });
    var vm = new Vue({
      el: '#app',
      data: {
        pmsg: '父组件中内容',
        pstr: 'hello',
        parr: ['apple','orange','banana'],
        pobj: {
          name: 'lisi',
          age: 12
        }
      }
    });
  </script>
</body>

子组件向父组件传值

  • 子组件用$emit()触发事件
  • $emit() 第一个参数为 自定义的事件名称 第二个参数为需要传递的数据
  • 父组件用v-on 监听子组件的事件
 <div id="app">
    <div :style='{fontSize: fontSize + "px"}'>{{pmsg}}</div>
     <!-- 2 父组件用v-on 监听子组件的事件
		这里 enlarge-text  是从 $emit 中的第一个参数对应   handle 为对应的事件处理函数	
	-->	
    <menu-item :parr='parr' @enlarge-text='handle($event)'></menu-item>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      子组件向父组件传值-携带参数
    */
    
    Vue.component('menu-item', {
      props: ['parr'],
      template: `
        <div>
          <ul>
            <li :key='index' v-for='(item,index) in parr'>{{item}}</li>
          </ul>
			###  1、子组件用$emit()触发事件
			### 第一个参数为 自定义的事件名称   第二个参数为需要传递的数据  
          <button @click='$emit("enlarge-text", 5)'>扩大父组件中字体大小</button>
          <button @click='$emit("enlarge-text", 10)'>扩大父组件中字体大小</button>
        </div>
      `
    });
    var vm = new Vue({
      el: '#app',
      data: {
        pmsg: '父组件中内容',
        parr: ['apple','orange','banana'],
        fontSize: 10
      },
      methods: {
        handle: function(val){
          // 扩大字体大小
          this.fontSize += val;
        }
      }
    });
  </script>

兄弟之间的传递

  • 兄弟之间传递数据需要借助于事件中心,通过事件中心传递数据
    • 提供事件中心 var hub = new Vue()
  • 传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据)
  • 接收数据方,通过mounted(){} 钩子中 触发hub.$on()方法名
  • 销毁事件 通过hub.$off()方法名销毁之后无法进行传递数据
 <div id="app">
    <div>父组件</div>
    <div>
      <button @click='handle'>销毁事件</button>
    </div>
    <test-tom></test-tom>
    <test-jerry></test-jerry>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      兄弟组件之间数据传递
    */
    //1、 提供事件中心
    var hub = new Vue();

    Vue.component('test-tom', {
      data: function(){
        return {
          num: 0
        }
      },
      template: `
        <div>
          <div>TOM:{{num}}</div>
          <div>
            <button @click='handle'>点击</button>
          </div>
        </div>
      `,
      methods: {
        handle: function(){
          //2、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据)   触发兄弟组件的事件
          hub.$emit('jerry-event', 2);
        }
      },
      mounted: function() {
       // 3、接收数据方,通过mounted(){} 钩子中  触发hub.$on(方法名
        hub.$on('tom-event', (val) => {
          this.num += val;
        });
      }
    });
    Vue.component('test-jerry', {
      data: function(){
        return {
          num: 0
        }
      },
      template: `
        <div>
          <div>JERRY:{{num}}</div>
          <div>
            <button @click='handle'>点击</button>
          </div>
        </div>
      `,
      methods: {
        handle: function(){
          //2、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据)   触发兄弟组件的事件
          hub.$emit('tom-event', 1);
        }
      },
      mounted: function() {
        // 3、接收数据方,通过mounted(){} 钩子中  触发hub.$on()方法名
        hub.$on('jerry-event', (val) => {
          this.num += val;
        });
      }
    });
    var vm = new Vue({
      el: '#app',
      data: {
        
      },
      methods: {
        handle: function(){
          //4、销毁事件 通过hub.$off()方法名销毁之后无法进行传递数据  
          hub.$off('tom-event');
          hub.$off('jerry-event');
        }
      }
    });
  </script>

组件插槽

  • 组件的最大特性就是复用性,而用好插槽能大大提高组件的可复用能力

匿名插槽

  <div id="app">
    <!-- 这里的所有组件标签中嵌套的内容会替换掉slot  如果不传值 则使用 slot 中的默认值  -->  
    <alert-box>有bug发生</alert-box>
    <alert-box>有一个警告</alert-box>
    <alert-box></alert-box>
  </div>

  <script type="text/javascript">
    /*
      组件插槽:父组件向子组件传递内容
    */
    Vue.component('alert-box', {
      template: `
        <div>
          <strong>ERROR:</strong>
		# 当组件渲染的时候,这个 <slot> 元素将会被替换为“组件标签中嵌套的内容”。
		# 插槽内可以包含任何模板代码,包括 HTML
          <slot>默认内容</slot>
        </div>
      `
    });
    var vm = new Vue({
      el: '#app',
      data: {
        
      }
    });
  </script>
</body>
</html>

具名插槽

  • 具有名字的插槽
  • 使用 中的 “name” 属性绑定元素
  <div id="app">
    <base-layout>
       <!-- 2、 通过slot属性来指定, 这个slot的值必须和下面slot组件得name值对应上
				如果没有匹配到 则放到匿名的插槽中   --> 
      <p slot='header'>标题信息</p>
      <p>主要内容1</p>
      <p>主要内容2</p>
      <p slot='footer'>底部信息信息</p>
    </base-layout>

    <base-layout>
      <!-- 注意点:template临时的包裹标签最终不会渲染到页面上     -->  
      <template slot='header'>
        <p>标题信息1</p>
        <p>标题信息2</p>
      </template>
      <p>主要内容1</p>
      <p>主要内容2</p>
      <template slot='footer'>
        <p>底部信息信息1</p>
        <p>底部信息信息2</p>
      </template>
    </base-layout>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      具名插槽
    */
    Vue.component('base-layout', {
      template: `
        <div>
          <header>
			###	1、 使用 <slot> 中的 "name" 属性绑定元素 指定当前插槽的名字
            <slot name='header'></slot>
          </header>
          <main>
            <slot></slot>
          </main>
          <footer>
			###  注意点: 
			###  具名插槽的渲染顺序,完全取决于模板,而不是取决于父组件中元素的顺序
            <slot name='footer'></slot>
          </footer>
        </div>
      `
    });
    var vm = new Vue({
      el: '#app',
      data: {
        
      }
    });
  </script>
</body>
</html>

作用域插槽

  • 父组件对子组件加工处理
  • 既可以复用子组件的slot,又可以使slot内容不一致
  <div id="app">
    <!-- 
		1、当我们希望li 的样式由外部使用组件的地方定义,因为可能有多种地方要使用该组件,
		但样式希望不一样 这个时候我们需要使用作用域插槽 
		
	-->  
    <fruit-list :list='list'>
       <!-- 2、 父组件中使用了<template>元素,而且包含scope="slotProps",
			slotProps在这里只是临时变量   
		---> 	
      <template slot-scope='slotProps'>
        <strong v-if='slotProps.info.id==3' class="current">
            {{slotProps.info.name}}		         
         </strong>
        <span v-else>{{slotProps.info.name}}</span>
      </template>
    </fruit-list>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    /*
      作用域插槽
    */
    Vue.component('fruit-list', {
      props: ['list'],
      template: `
        <div>
          <li :key='item.id' v-for='item in list'>
			###  3、 在子组件模板中,<slot>元素上有一个类似props传递数据给组件的写法msg="xxx",
			###   插槽可以提供一个默认内容,如果如果父组件没有为这个插槽提供了内容,会显示默认的内容。
					如果父组件为这个插槽提供了内容,则默认的内容会被替换掉
            <slot :info='item'>{{item.name}}</slot>
          </li>
        </div>
      `
    });
    var vm = new Vue({
      el: '#app',
      data: {
        list: [{
          id: 1,
          name: 'apple'
        },{
          id: 2,
          name: 'orange'
        },{
          id: 3,
          name: 'banana'
        }]
      }
    });
  </script>
</body>
</html>

购物车案例

1. 实现组件化布局

  • 把静态页面转换成组件化模式
  • 把组件渲染到页面上
 <div id="app">
    <div class="container">
      <!-- 2、把组件渲染到页面上 --> 
      <my-cart></my-cart>
    </div>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    # 1、 把静态页面转换成组件化模式
    # 1.1  标题组件 
    var CartTitle = {
      template: `
        <div class="title">我的商品</div>
      `
    }
    # 1.2  商品列表组件 
    var CartList = {
      #  注意点 :  组件模板必须是单个根元素  
      template: `
        <div>
          <div class="item">
            <img src="img/a.jpg"/>
            <div class="name"></div>
            <div class="change">
              <a href="">-</a>
              <input type="text" class="num" />
              <a href="">+</a>
            </div>
            <div class="del">×</div>
          </div>
          <div class="item">
            <img src="img/b.jpg"/>
            <div class="name"></div>
            <div class="change">
              <a href="">-</a>
              <input type="text" class="num" />
              <a href="">+</a>
            </div>
            <div class="del">×</div>
          </div>
          <div class="item">
            <img src="img/c.jpg"/>
            <div class="name"></div>
            <div class="change">
              <a href="">-</a>
              <input type="text" class="num" />
              <a href="">+</a>
            </div>
            <div class="del">×</div>
          </div>
          <div class="item">
            <img src="img/d.jpg"/>
            <div class="name"></div>
            <div class="change">
              <a href="">-</a>
              <input type="text" class="num" />
              <a href="">+</a>
            </div>
            <div class="del">×</div>
          </div>
          <div class="item">
            <img src="img/e.jpg"/>
            <div class="name"></div>
            <div class="change">
              <a href="">-</a>
              <input type="text" class="num" />
              <a href="">+</a>
            </div>
            <div class="del">×</div>
          </div>
        </div>
      `
    }
    # 1.3  商品结算组件 
    var CartTotal = {
      template: `
        <div class="total">
          <span>总价:123</span>
          <button>结算</button>
        </div>
      `
    }
    ## 1.4  定义一个全局组件 my-cart
    Vue.component('my-cart',{
      ##  1.6 引入子组件  
      template: `
        <div class='cart'>
          <cart-title></cart-title>
          <cart-list></cart-list>
          <cart-total></cart-total>
        </div>
      `,
      # 1.5  注册子组件   
      components: {
        'cart-title': CartTitle,
        'cart-list': CartList,
        'cart-total': CartTotal
      }
    });
    var vm = new Vue({
      el: '#app',
      data: {

      }
    });

  </script>



2、实现 标题和结算功能组件

  • 标题组件实现动态渲染
    • 从父组件把标题数据传递过来 即 父向子组件传值
    • 把传递过来的数据渲染到页面上
  • 结算功能组件
    • 从父组件把商品列表list 数据传递过来 即 父向子组件传值
    • 把传递过来的数据计算最终价格渲染到页面上
 <div id="app">
    <div class="container">
      <my-cart></my-cart>
    </div>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
     # 2.2  标题组件     子组件通过props形式接收父组件传递过来的uname数据
    var CartTitle = {
      props: ['uname'],
      template: `
        <div class="title">{{uname}}的商品</div>
      `
    }
	# 2.3  商品结算组件  子组件通过props形式接收父组件传递过来的list数据   
    var CartTotal = {
      props: ['list'],
      template: `
        <div class="total">
          <span>总价:{{total}}</span>
          <button>结算</button>
        </div>
      `,
      computed: {
        # 2.4    计算商品的总价  并渲染到页面上 
        total: function() {
          var t = 0;
          this.list.forEach(item => {
            t += item.price * item.num;
          });
          return t;
        }
      }
    }
    Vue.component('my-cart',{
      data: function() {
        return {
          uname: '张三',
          list: [{
            id: 1,
            name: 'TCL彩电',
            price: 1000,
            num: 1,
            img: 'img/a.jpg'
          },{
            id: 2,
            name: '机顶盒',
            price: 1000,
            num: 1,
            img: 'img/b.jpg'
          },{
            id: 3,
            name: '海尔冰箱',
            price: 1000,
            num: 1,
            img: 'img/c.jpg'
          },{
            id: 4,
            name: '小米手机',
            price: 1000,
            num: 1,
            img: 'img/d.jpg'
          },{
            id: 5,
            name: 'PPTV电视',
            price: 1000,
            num: 2,
            img: 'img/e.jpg'
          }]
        }
      },
      #  2.1  父组件向子组件以属性传递的形式 传递数据
      #   向 标题组件传递 uname 属性   向 商品结算组件传递 list  属性  
      template: `
        <div class='cart'>
          <cart-title :uname='uname'></cart-title>
          <cart-list></cart-list>
          <cart-total :list='list'></cart-total>
        </div>
      `,
      components: {
        'cart-title': CartTitle,
        'cart-list': CartList,
        'cart-total': CartTotal
      }
    });
    var vm = new Vue({
      el: '#app',
      data: {

      }
    });

  </script>

3. 实现列表组件删除功能

  • 从父组件把商品列表list 数据传递过来 即 父向子组件传值
  • 把传递过来的数据渲染到页面上
  • 点击删除按钮的时候删除对应的数据
    • 给按钮添加点击事件把需要删除的id传递过来
      • 子组件中不推荐操作父组件的数据有可能多个子组件使用父组件的数据 我们需要把数据传递给父组件让父组件操作数据
      • 父组件删除对应的数据
 <div id="app">
    <div class="container">
      <my-cart></my-cart>
    </div>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    
    var CartTitle = {
      props: ['uname'],
      template: `
        <div class="title">{{uname}}的商品</div>
      `
    }
    #  3.2 把列表数据动态渲染到页面上  
    var CartList = {
      props: ['list'],
      template: `
        <div>
          <div :key='item.id' v-for='item in list' class="item">
            <img :src="item.img"/>
            <div class="name">{{item.name}}</div>
            <div class="change">
              <a href="">-</a>
              <input type="text" class="num" />
              <a href="">+</a>
            </div>
			# 3.3  给按钮添加点击事件把需要删除的id传递过来
            <div class="del" @click='del(item.id)'>×</div>
          </div>
        </div>
      `,
      methods: {
        del: function(id){
           # 3.4 子组件中不推荐操作父组件的数据有可能多个子组件使用父组件的数据 
          # 	  我们需要把数据传递给父组件 让父组件操作数据 
          this.$emit('cart-del', id);
        }
      }
    }
    var CartTotal = {
      props: ['list'],
      template: `
        <div class="total">
          <span>总价:{{total}}</span>
          <button>结算</button>
        </div>
      `,
      computed: {
        total: function() {
          // 计算商品的总价
          var t = 0;
          this.list.forEach(item => {
            t += item.price * item.num;
          });
          return t;
        }
      }
    }
    Vue.component('my-cart',{
      data: function() {
        return {
          uname: '张三',
          list: [{
            id: 1,
            name: 'TCL彩电',
            price: 1000,
            num: 1,
            img: 'img/a.jpg'
          },{
            id: 2,
            name: '机顶盒',
            price: 1000,
            num: 1,
            img: 'img/b.jpg'
          },{
            id: 3,
            name: '海尔冰箱',
            price: 1000,
            num: 1,
            img: 'img/c.jpg'
          },{
            id: 4,
            name: '小米手机',
            price: 1000,
            num: 1,
            img: 'img/d.jpg'
          },{
            id: 5,
            name: 'PPTV电视',
            price: 1000,
            num: 2,
            img: 'img/e.jpg'
          }]
        }
      },
      # 3.1 从父组件把商品列表list 数据传递过来 即 父向子组件传值  
      template: `
        <div class='cart'>
          <cart-title :uname='uname'></cart-title>
		  #  3.5  父组件通过事件绑定 接收子组件传递过来的数据 
          <cart-list :list='list' @cart-del='delCart($event)'></cart-list>
          <cart-total :list='list'></cart-total>
        </div>
      `,
      components: {
        'cart-title': CartTitle,
        'cart-list': CartList,
        'cart-total': CartTotal
      },
      methods: {
        # 3.6    根据id删除list中对应的数据        
        delCart: function(id) {
          // 1、找到id所对应数据的索引
          var index = this.list.findIndex(item=>{
            return item.id == id;
          });
          // 2、根据索引删除对应数据
          this.list.splice(index, 1);
        }
      }
    });
    var vm = new Vue({
      el: '#app',
      data: {

      }
    });

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

4. 实现组件更新数据功能 上

  • 将输入框中的默认数据动态渲染出来
  • 输入框失去焦点的时候 更改商品的数量
  • 子组件中不推荐操作数据 把这些数据传递给父组件 让父组件处理这些数据
  • 父组件中接收子组件传递过来的数据并处理
 <div id="app">
    <div class="container">
      <my-cart></my-cart>
    </div>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    
    var CartTitle = {
      props: ['uname'],
      template: `
        <div class="title">{{uname}}的商品</div>
      `
    }
    var CartList = {
      props: ['list'],
      template: `
        <div>
          <div :key='item.id' v-for='item in list' class="item">
            <img :src="item.img"/>
            <div class="name">{{item.name}}</div>
            <div class="change">
              <a href="">-</a>
				# 1. 将输入框中的默认数据动态渲染出来
				# 2. 输入框失去焦点的时候 更改商品的数量  需要将当前商品的id 传递过来
              <input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
              <a href="">+</a>
            </div>
            <div class="del" @click='del(item.id)'>×</div>
          </div>
        </div>
      `,
      methods: {
        changeNum: function(id, event){
          # 3 子组件中不推荐操作数据  因为别的组件可能也引用了这些数据
          #  把这些数据传递给父组件 让父组件处理这些数据
          this.$emit('change-num', {
            id: id,
            num: event.target.value
          });
        },
        del: function(id){
          // 把id传递给父组件
          this.$emit('cart-del', id);
        }
      }
    }
    var CartTotal = {
      props: ['list'],
      template: `
        <div class="total">
          <span>总价:{{total}}</span>
          <button>结算</button>
        </div>
      `,
      computed: {
        total: function() {
          // 计算商品的总价
          var t = 0;
          this.list.forEach(item => {
            t += item.price * item.num;
          });
          return t;
        }
      }
    }
    Vue.component('my-cart',{
      data: function() {
        return {
          uname: '张三',
          list: [{
            id: 1,
            name: 'TCL彩电',
            price: 1000,
            num: 1,
            img: 'img/a.jpg'
          }]
      },
      template: `
        <div class='cart'>
          <cart-title :uname='uname'></cart-title>
			# 4  父组件中接收子组件传递过来的数据 
          <cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list>
          <cart-total :list='list'></cart-total>
        </div>
      `,
      components: {
        'cart-title': CartTitle,
        'cart-list': CartList,
        'cart-total': CartTotal
      },
      methods: {
        changeNum: function(val) {
          //4.1 根据子组件传递过来的数据,跟新list中对应的数据
          this.list.some(item=>{
            if(item.id == val.id) {
              item.num = val.num;
              // 终止遍历
              return true;
            }
          });
        },
        delCart: function(id) {
          // 根据id删除list中对应的数据
          // 1、找到id所对应数据的索引
          var index = this.list.findIndex(item=>{
            return item.id == id;
          });
          // 2、根据索引删除对应数据
          this.list.splice(index, 1);
        }
      }
    });
    var vm = new Vue({
      el: '#app',
      data: {

      }
    });

  </script>

5. 实现组件更新数据功能 下

  • 子组件通过一个标识符来标记对用的用户点击 + - 或者输入框输入的内容
  • 父组件拿到标识符更新对应的组件
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <style type="text/css">
    .container {
    }
    .container .cart {
      width: 300px;
      margin: auto;
    }
    .container .title {
      background-color: lightblue;
      height: 40px;
      line-height: 40px;
      text-align: center;
      /*color: #fff;*/  
    }
    .container .total {
      background-color: #FFCE46;
      height: 50px;
      line-height: 50px;
      text-align: right;
    }
    .container .total button {
      margin: 0 10px;
      background-color: #DC4C40;
      height: 35px;
      width: 80px;
      border: 0;
    }
    .container .total span {
      color: red;
      font-weight: bold;
    }
    .container .item {
      height: 55px;
      line-height: 55px;
      position: relative;
      border-top: 1px solid #ADD8E6;
    }
    .container .item img {
      width: 45px;
      height: 45px;
      margin: 5px;
    }
    .container .item .name {
      position: absolute;
      width: 90px;
      top: 0;left: 55px;
      font-size: 16px;
    }

    .container .item .change {
      width: 100px;
      position: absolute;
      top: 0;
      right: 50px;
    }
    .container .item .change a {
      font-size: 20px;
      width: 30px;
      text-decoration:none;
      background-color: lightgray;
      vertical-align: middle;
    }
    .container .item .change .num {
      width: 40px;
      height: 25px;
    }
    .container .item .del {
      position: absolute;
      top: 0;
      right: 0px;
      width: 40px;
      text-align: center;
      font-size: 40px;
      cursor: pointer;
      color: red;
    }
    .container .item .del:hover {
      background-color: orange;
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="container">
      <my-cart></my-cart>
    </div>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    
    var CartTitle = {
      props: ['uname'],
      template: `
        <div class="title">{{uname}}的商品</div>
      `
    }
    var CartList = {
      props: ['list'],
      template: `
        <div>
          <div :key='item.id' v-for='item in list' class="item">
            <img :src="item.img"/>
            <div class="name">{{item.name}}</div>
            <div class="change">
			  # 1.  + - 按钮绑定事件 
              <a href="" @click.prevent='sub(item.id)'>-</a>
              <input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
              <a href="" @click.prevent='add(item.id)'>+</a>
            </div>
            <div class="del" @click='del(item.id)'>×</div>
          </div>
        </div>
      `,
      methods: {
        changeNum: function(id, event){
          this.$emit('change-num', {
            id: id,
            type: 'change',
            num: event.target.value
          });
        },
        sub: function(id){
          # 2 数量的增加和减少通过父组件来计算   每次都是加1 和 减1 不需要传递数量   父组件需要一个类型来判断 是 加一 还是减1  以及是输入框输入的数据  我们通过type 标识符来标记 不同的操作   
          this.$emit('change-num', {
            id: id,
            type: 'sub'
          });
        },
        add: function(id){
         # 2 数量的增加和减少通过父组件来计算   每次都是加1 和 减1 不需要传递数量   父组件需要一个类型来判断 是 加一 还是减1  以及是输入框输入的数据  我们通过type 标识符来标记 不同的操作
          this.$emit('change-num', {
            id: id,
            type: 'add'
          });
        },
        del: function(id){
          // 把id传递给父组件
          this.$emit('cart-del', id);
        }
      }
    }
    var CartTotal = {
      props: ['list'],
      template: `
        <div class="total">
          <span>总价:{{total}}</span>
          <button>结算</button>
        </div>
      `,
      computed: {
        total: function() {
          // 计算商品的总价
          var t = 0;
          this.list.forEach(item => {
            t += item.price * item.num;
          });
          return t;
        }
      }
    }
    Vue.component('my-cart',{
      data: function() {
        return {
          uname: '张三',
          list: [{
            id: 1,
            name: 'TCL彩电',
            price: 1000,
            num: 1,
            img: 'img/a.jpg'
          },{
            id: 2,
            name: '机顶盒',
            price: 1000,
            num: 1,
            img: 'img/b.jpg'
          },{
            id: 3,
            name: '海尔冰箱',
            price: 1000,
            num: 1,
            img: 'img/c.jpg'
          },{
            id: 4,
            name: '小米手机',
            price: 1000,
            num: 1,
            img: 'img/d.jpg'
          },{
            id: 5,
            name: 'PPTV电视',
            price: 1000,
            num: 2,
            img: 'img/e.jpg'
          }]
        }
      },
      template: `
        <div class='cart'>
          <cart-title :uname='uname'></cart-title>	
		# 3 父组件通过事件监听   接收子组件的数据  
          <cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list>
          <cart-total :list='list'></cart-total>
        </div>
      `,
      components: {
        'cart-title': CartTitle,
        'cart-list': CartList,
        'cart-total': CartTotal
      },
      methods: {
        changeNum: function(val) {
          #4 分为三种情况:输入框变更、加号变更、减号变更
          if(val.type=='change') {
            // 根据子组件传递过来的数据,跟新list中对应的数据
            this.list.some(item=>{
              if(item.id == val.id) {
                item.num = val.num;
                // 终止遍历
                return true;
              }
            });
          }else if(val.type=='sub'){
            // 减一操作
            this.list.some(item=>{
              if(item.id == val.id) {
                item.num -= 1;
                // 终止遍历
                return true;
              }
            });
          }else if(val.type=='add'){
            // 加一操作
            this.list.some(item=>{
              if(item.id == val.id) {
                item.num += 1;
                // 终止遍历
                return true;
              }
            });
          }
        }
      }
    });
    var vm = new Vue({
      el: '#app',
      data: {

      }
    });

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

6. 源代码

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

<head>
    <meta charset="UTF-8" />
    <title>Document</title>
    <style type="text/css">
        .container {}

        .container .cart {
            width: 300px;
            margin: auto;
        }

        .container .title {
            background-color: lightblue;
            height: 40px;
            line-height: 40px;
            text-align: center;
            /*color: #fff;*/
        }

        .container .total {
            background-color: #ffce46;
            height: 50px;
            line-height: 50px;
            text-align: right;
        }

        .container .total button {
            margin: 0 10px;
            background-color: #dc4c40;
            height: 35px;
            width: 80px;
            border: 0;
        }

        .container .total span {
            color: red;
            font-weight: bold;
        }

        .container .item {
            height: 55px;
            line-height: 55px;
            position: relative;
            border-top: 1px solid #add8e6;
        }

        .container .item img {
            width: 45px;
            height: 45px;
            margin: 5px;
        }

        .container .item .name {
            position: absolute;
            width: 90px;
            top: 0;
            left: 55px;
            font-size: 16px;
        }

        .container .item .change {
            width: 100px;
            position: absolute;
            top: 0;
            right: 50px;
        }

        .container .item .change a {
            font-size: 20px;
            width: 30px;
            text-decoration: none;
            background-color: lightgray;
            vertical-align: middle;
        }

        .container .item .change .num {
            width: 40px;
            height: 25px;
        }

        .container .item .del {
            position: absolute;
            top: 0;
            right: 0px;
            width: 40px;
            text-align: center;
            font-size: 40px;
            cursor: pointer;
            color: red;
        }

        .container .item .del:hover {
            background-color: orange;
        }
    </style>
</head>

<body>
    <div id="app">
        <div class="container">
            <my-cart></my-cart>
        </div>
    </div>
    <script type="text/javascript" src="js/vue.js"></script>
    <script type="text/javascript">
        var CartTitle = {
            props: ['uname'],
            template: `
            <div class="title">{{uname}}的商品</div>
        `,
        }
        var CartList = {
            props: ['list'],
            template: `
            <div>
                <div :key='item.id' v-for='item in list' class="item">
                    <img :src="item.img"/>
                    <div class="name">{{item.name}}</div>
                    <div class="change">
                        <a href="" @click.prevent='sub(item.id)'>-</a>
                        <input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
                        <a href="" @click.prevent='add(item.id)'>+</a>
                    </div>
                    <div class="del" @click='del(item.id)'>×</div>
                </div>
            </div>
        `,
            methods: {
                changeNum: function (id, event) {
                    this.$emit('change-num', {
                        id: id,
                        type: 'change',
                        num: event.target.value,
                    })
                },
                sub: function (id) {
                    this.$emit('change-num', {
                        id: id,
                        type: 'sub',
                    })
                },
                add: function (id) {
                    this.$emit('change-num', {
                        id: id,
                        type: 'add',
                    })
                },
                del: function (id) {
                    // 把id传递给父组件
                    this.$emit('cart-del', id)
                },
            },
        }
        var CartTotal = {
            props: ['list'],
            template: `
            <div class="total">
                <span>总价:{{total}}</span>
                <button>结算</button>
            </div>
        `,
            computed: {
                total: function () {
                    // 计算商品的总价
                    var t = 0
                    this.list.forEach((item) => {
                        t += item.price * item.num
                    })
                    return t
                },
            },
        }
        Vue.component('my-cart', {
            data: function () {
                return {
                    uname: '张三',
                    list: [{
                            id: 1,
                            name: 'TCL彩电',
                            price: 1000,
                            num: 1,
                            img: 'img/a.jpg',
                        },
                        {
                            id: 2,
                            name: '机顶盒',
                            price: 1000,
                            num: 1,
                            img: 'img/b.jpg',
                        },
                        {
                            id: 3,
                            name: '海尔冰箱',
                            price: 1000,
                            num: 1,
                            img: 'img/c.jpg',
                        },
                        {
                            id: 4,
                            name: '小米手机',
                            price: 1000,
                            num: 1,
                            img: 'img/d.jpg',
                        },
                        {
                            id: 5,
                            name: 'PPTV电视',
                            price: 1000,
                            num: 2,
                            img: 'img/e.jpg',
                        },
                    ],
                }
            },
            template: `
            <div class='cart'>
                <cart-title :uname='uname'></cart-title>
                <cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list>
                <cart-total :list='list'></cart-total>
            </div>
        `,
            components: {
                'cart-title': CartTitle,
                'cart-list': CartList,
                'cart-total': CartTotal,
            },
            methods: {
                changeNum: function (val) {
                    // 分为三种情况:输入域变更、加号变更、减号变更
                    if (val.type == 'change') {
                        // 根据子组件传递过来的数据,跟新list中对应的数据
                        this.list.some((item) => {
                            if (item.id == val.id) {
                                item.num = val.num
                                // 终止遍历
                                return true
                            }
                        })
                    } else if (val.type == 'sub') {
                        // 减一操作
                        this.list.some((item) => {
                            if (item.id == val.id) {
                                item.num -= 1
                                // 终止遍历
                                return true
                            }
                        })
                    } else if (val.type == 'add') {
                        // 加一操作
                        this.list.some((item) => {
                            if (item.id == val.id) {
                                item.num += 1
                                // 终止遍历
                                return true
                            }
                        })
                    }
                },
                delCart: function (id) {
                    // 根据id删除list中对应的数据
                    // 1、找到id所对应数据的索引
                    var index = this.list.findIndex((item) => {
                        return item.id == id
                    })
                    // 2、根据索引删除对应数据
                    this.list.splice(index, 1)
                },
            },
        })
        var vm = new Vue({
            el: '#app',
            data: {},
        })
    </script>
</body>

</html>在这里插入代码片

每日作业-Vue第03天

todos案例-组件化抽离

  • 题目描述

    • 当在输入框中输入完内容的按回车键 当前内容展示到页面上
    • 点击三角 实现全选和全不选功能
    • 点击叉号实现删除功能
    • 双击标题实现编辑功能
    • 如 点击 SECTION 1 则 内容区域显示 对应 SECTION 1 的内容 同时当前 SECTION的字体颜色变成蓝色
  • 训练目标

    • 能够理解vue 中的数据渲染
    • 能够理解 v-for, v-if , v-bind , v-click 的使用
    • 能够理解 vue 中自定义指令、计算属性、数组变异方法
    • 组件化封装
  • 训练提示

    • 提供的数据如下

      • 1、引入todos的CSS样式 (HTML和 CSS 已经写好 我们需要改成Vue动态渲染的
       <!-- HTML   -->
          <section id="todoapp" class="todoapp">
              <header class="header">
                  <h1>todos</h1>
                  <input placeholder="What needs to be done?" class="new-todo">
              </header>
              <section class="main">
                  	<input id="toggle-all" type="checkbox" class="toggle-all"> 
                  	<label for="toggle-all">Mark all as complete</label>
                  <ul class="todo-list">
                      <li class="">
                          <div class="view"><input type="checkbox" class="toggle"> 
                              <label>吃饭</label> 
                              <button class="destroy"></button>
                          </div> 
                          <input class="edit">
                      </li>
                      <li class="">
                          <div class="view">
                              <input type="checkbox" class="toggle">
                              <label>睡觉</label> 
                              <button class="destroy"></button>
                          </div> <input class="edit"></li>
                      <li class="completed">
                          <div class="view">
                              <input type="checkbox" class="toggle"> 
                              <label>打豆豆</label> 
                              <button class="destroy">
                              </button></div> 
                          <input class="edit">
                      </li>
                  </ul>
              </section>
              <footer class="footer">
                  <span class="todo-count">
                      <strong>2</strong> item left</span>
                  <ul class="filters">
                      <li><a href="#/" class="selected">All</a></li>
                      <li><a href="#/active">Active</a></li>
                      <li><a href="#/completed">Completed</a></li>
                  </ul> 
                  <button class="clear-completed">Clear completed</button>
              </footer>
          </section>
      
      
      
      <!---   2CSS -->
          <link rel="stylesheet" href="css/base.css">
          <link rel="stylesheet" href="css/index.css">
      
      <!---   3、 提供的数据  -->
      
          <script>
              new Vue({
                  el: "#todoapp",
                  data: {
                      todos: [{
                          id: 1,
                          title: '吃饭',
                          completed: false
                      }, {
                          id: 2,
                          title: '睡觉',
                          completed: false
                      }, {
                          id: 3,
                          title: '打豆豆',
                          completed: true
                      }]
                  }
              })
          </script>
      
  • 操作步骤

    • 2 抽离头部组件

      • 把头部封装到一个组件中
      • 给输入框绑定事件
      • 当用户输入完数据后 通过子向父传值 把获取的用户输入的信息提交到父组件中去
      • 父组件接收到子组件传递的数据 存放到todos 中
      • 父组件中展示头部组件
    • 3 抽离数据展示组件

      • 3.1 把显示数据的代码封装到一个组件中
      • 3.2 把父组件中的todos 传递过来 子组件接收到父组件传递过来的数据 进行渲染
      • 3.3 点击删除 删除当前的数据
      • 3.4 双击的时候 当前数据可编辑
      • 3.5 按enter键的时候 保存当前数据
      • 3.6 实现全选功能
    • 4 抽离fotter 部分

      • 4.1 把footer 部分的代码封装到一个组件中
      • 4.2 实现 未选中部分的展示
      • 4.3 删除已经选中的

每日作业-Vue第03天参考答案

todos案例-组件化抽离

1 提供的数据和 HTML结构

  • 引入todos的CSS样式 (HTML和 CSS 已经写好 我们需要改成Vue动态渲染的)
  <!-- HTML   -->
    <section id="todoapp" class="todoapp">
        <header class="header">
            <h1>todos</h1>
            <input placeholder="What needs to be done?" class="new-todo">
        </header>
        <section class="main">
            	<input id="toggle-all" type="checkbox" class="toggle-all"> 
            	<label for="toggle-all">Mark all as complete</label>
            <ul class="todo-list">
                <li class="">
                    <div class="view"><input type="checkbox" class="toggle"> 
                        <label>吃饭</label> 
                        <button class="destroy"></button>
                    </div> 
                    <input class="edit">
                </li>
                <li class="">
                    <div class="view">
                        <input type="checkbox" class="toggle">
                        <label>睡觉</label> 
                        <button class="destroy"></button>
                    </div> <input class="edit"></li>
                <li class="completed">
                    <div class="view">
                        <input type="checkbox" class="toggle"> 
                        <label>打豆豆</label> 
                        <button class="destroy">
                        </button></div> 
                    <input class="edit">
                </li>
            </ul>
        </section>
        <footer class="footer">
            <span class="todo-count">
                <strong>2</strong> item left</span>
            <ul class="filters">
                <li><a href="#/" class="selected">All</a></li>
                <li><a href="#/active">Active</a></li>
                <li><a href="#/completed">Completed</a></li>
            </ul> 
            <button class="clear-completed">Clear completed</button>
        </footer>
    </section>



<!---   2CSS -->
    <link rel="stylesheet" href="css/base.css">
    <link rel="stylesheet" href="css/index.css">

<!---   3、 提供的数据  -->

    <script>
        new Vue({
            el: "#todoapp",
            data: {
                todos: [{
                    id: 1,
                    title: '吃饭',
                    completed: false
                }, {
                    id: 2,
                    title: '睡觉',
                    completed: false
                }, {
                    id: 3,
                    title: '打豆豆',
                    completed: true
                }]
            }
        })
    </script>

2 抽离头部组件

  • 把头部封装到一个组件中
  • 给输入框绑定事件
  • 当用户输入完数据后 通过子向父传值 把获取的用户输入的信息提交到父组件中去
  • 父组件接收到子组件传递的数据 存放到todos 中
  • 父组件中展示头部组件
  <section id="todoapp" class="todoapp">
      	<!-- 2.2 父组件中展示头部组件
			2.3 父组件接收到子组件传递的数据
		-->
        <myheader @inpvalue="addTodo"></myheader>

   </section>

<script>
     //   1、把header  部分提取出来 
        var myheader = {
                template: `
                <header class="header">
                    <h1>todos</h1>
                    <input placeholder="What needs to be done?"
                    @keyup.enter="addToParent" class="new-todo">
                </header>
            `,
                methods: {
                    //1.1  把用户输入的数据传递到父组件中去
                    addToParent(event) {
                        var todoText = event.target.value.trim()
                        if (!todoText.length) {
                            return
                        }
                        this.$emit("inpvalue", todoText)
                    }
                }
            }
        
       new Vue({
            el: "#todoapp",
            data: {
                currentEditing: null,
                todos: [{
                    id: 1,
                    title: '吃饭',
                    completed: false
                }, {
                    id: 2,
                    title: '睡觉',
                    completed: false
                }, {
                    id: 3,
                    title: '打豆豆',
                    completed: true
                }]
            },
        	  methods: {
                addTodo(todoText) {
                    # 2.4 父组件接收到子组件传递的数据 存放到todos 中 
                    const lastTodo = this.todos[this.todos.length - 1]
                    const id = lastTodo ? lastTodo.id + 1 : 1
                    //当数组发生变化,则绑定渲染该数组的视图也会得到更新
                    this.todos.push({
                        id,
                        title: todoText,
                        completed: false
                    })
                    // 清空文本框
                    event.target.value = ''
                },
             }
           # 2.1 注册子组件
            components: {
           
                myheader
            }
        })
</script>

3 抽离数据展示组件

  • 3.1 把显示数据的代码封装到一个组件中
  • 3.2 把父组件中的todos 传递过来 子组件接收到父组件传递过来的数据 进行渲染
  • 3.3 点击删除 删除当前的数据
  • 3.4 双击的时候 当前数据可编辑
  • 3.5 按enter键的时候 保存当前数据
  • 3.6 实现全选功能
    <section id="todoapp" class="todoapp">
        <!-- 3.2.1  父组件通过属性绑定 :todos="todos"   把数据传递给子组件  -->
        
        <!-- 3.3.3   父组件通过 事件监听 @removetodo="removeTodo"   接收子组件传递过来的数据  -->
        <!--  3.4.2   父组件通过 事件监听   @dbl-click="aaaa"   接收子组件传递过来的数据         -->
        <mylist :todos="todos" @dbl-click="aaaa" 
                @removetodo="removeTodo"
                @save-edit="saveEdit" 
                :currentediting="currentEditing" 
                >
        </mylist>

    </section>
    <script src="js/vue.js"></script>

    <script>
        var mylist = {
            # 3.2.2   子组件通过 props 接收父组件传递过来的数据
            props: ["todos", "currentediting"],
            template: ` 
            <section class="main">
				# 3.6 实现全选功能    通过双向绑定   计算属性  toggleStat1
               <input v-model="toggleStat1" id="toggle-all" 
  				type="checkbox" class="toggle-all">
                <label for="toggle-all">Mark all as complete</label>
                <ul class="todo-list">
					  # 3.2.3   把todos 展示到当前页面
                        <li v-for="(item, index) in todos" 
                        v-bind:class="{completed: item.completed,
						editing: item === currentediting}">
                            <div class="view">
                                <input type="checkbox" class="toggle"
									v-model="item.completed">
			
							# 3.4 双击的时候 当前数据可编辑  
							# 3.4.1 添加双击事件 我们通过   类名  editing  来控制当前输入框显示
							# 在父组件中定义一个标识符 currentEditing  默认为 空 当我们双击的时候
							# 给当前点击的li 添加  editing  类名
					
                                <label @dblclick="doubleMethods(item)">
									{{item.title}}</label>
						
								#3.3 点击删除  删除当前的数据  
								#3.3.1 给按钮绑定事件 需要把当前的id 传入过来
	
                                <button class="destroy" 
									@click="removeTodoParent(index, $event)">
        						</button>
                            </div>
					
							# 3.5  按enter键的时候 保存当前数据 
                            <input class="edit" 
						@keyup.enter="saveEdit(item, index, $event)"
					:value="item.title" 
						@keyup.esc="currentediting = null">
                        </li>
                </ul>
            </section>
            `,
            methods: {
                #  3.3.2  删除操作  子组件不要操作父组件里面的数据 把对应的数据传递到父组件中
                
                removeTodoParent(index) {
                    this.$emit("removetodo", index)
                },
                # 3.5.1  把当前数据发送到父元素     通过父元素保存子元素数据   
                saveEdit(item, index, event) {
                    var editText = event.target.value.trim()
                    this.$emit("save-edit", editText, item, index)
                },
				# 3.4.2   子组件 把当前点击的li 传递到父元素                      
                doubleMethods(item) {
                    this.$emit("dbl-click", item)
                }
            },
            computed: {
                # 3.6.1  实现全选功能  
                toggleStat1: {
                    get() {
                        return this.todos.every(item => item.completed)
                    },
                    set(val) {
                        this.todos.forEach(todo => todo.completed = val)
                    }
                },
            },
        }

        new Vue({
            el: "#todoapp",
            data: {
                currentEditing: null,
                todos: [{
                    id: 1,
                    title: '吃饭',
                    completed: false
                }, {
                    id: 2,
                    title: '睡觉',
                    completed: false
                }, {
                    id: 3,
                    title: '打豆豆',
                    completed: true
                }]
            },
            methods: {
                // 3.3.4     删除任务项   根据子组件传递过来的 id 删除对应的数据
                removeTodo(delIndex) {
                    this.todos.splice(delIndex, 1)
                },
                // 保存编辑项
                saveEdit(editText, item, index) {
                    console.log(item, index, '-------------')
                        // 1. 拿到文本框中的数据
                        //    非空校验
                        //    如果为空,则直接删除这个 item
                        //    如果不为空,则修改任务项的 title 数据


                    // 程序员要具有工匠精神:优化简写
                    // !editText.length ?
                    //   this.todos.splice(index, 1) :
                    //   item.title = editText

                    if (!editText.length) {
                        // 将元素直接从数组中移除
                        return this.todos.splice(index, 1)
                    }

                    // 2. 将数据设置到任务项中
                    item.title = editText

                    // 3. 去除 editing 样式
                    this.currentEditing = null
                },
                // 删除所有已完成任务项
                removeAllDone() {

                    this.todos = this.todos.filter(item => !item.completed)

                },
               # 3.4.3    把当前li的数据 赋值给 currentEditing 即让当前li 绑定 类名
                aaaa(item) {
                    this.currentEditing = item
                }
            },
            computed: {


            },
            components: {
                myheader,
                mylist,
            }
        })
    </script>
</body>

</html>


4 抽离fotter 部分

  • 4.1 把footer 部分的代码封装到一个组件中
  • 4.2 实现 未选中部分的展示
  • 4.3 删除已经选中的
  <section id="todoapp" class="todoapp">
    
        <myfotter :leftcount="leftCount" @delete-all-done="removeAllDone"></myfotter>

    </section>

<script>
		# 4.1 把footer 部分的代码封装到一个组件中 
        var myfotter = {
            props: ['leftcount'],
            template: `
            <footer class="footer">
            <span class="todo-count"><strong>{{leftcount}}</strong> item left</span>
            <button class="clear-completed" @click="deleteAll">Clear completed</button>
        </footer>
            `,
            methods: {
                deleteAll() {
                    this.$emit("delete-all-done")
                }
            }
        }
        
        new Vue({
     
             computed: {

                leftCount: function() {
                    return this.todos.filter(item => !item.completed).length
                },

            },
            components: {
                myheader,
                mylist,
                myfotter
            }
        })
	
</script>

5 源代码

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="css/base.css">
    <link rel="stylesheet" href="css/index.css">
</head>

<body>

    <section id="todoapp" class="todoapp">
        <myheader @inpvalue="addTodo"></myheader>
        <mylist :todos="todos" @dbl-click="aaaa" @save-edit="saveEdit" :currentediting="currentEditing" @removetodo="removeTodo"></mylist>
        <myfotter :leftcount="leftCount" @delete-all-done="removeAllDone"></myfotter>

    </section>
    <script src="js/vue.js"></script>

    <script>
        //   1、把header  部分提取出来 
        var myheader = {
                template: `
            <header class="header">
                <h1>todos</h1>
                <input placeholder="What needs to be done?" @keyup.enter="addToParent" class="new-todo">
            </header>
            `,
                methods: {
                    //1.1  把用户输入的数据传递到父组件中去
                    addToParent(event) {
                        var todoText = event.target.value.trim()
                        if (!todoText.length) {
                            return
                        }
                        this.$emit("inpvalue", todoText)
                    }
                }
            }
            // 2.1  把 展示数据列表提取出来
            // 2.2 把父组件中的todos 传递过来  子组件接收到父组件传递过来的数据 进行渲染
        var mylist = {
            props: ["todos", "currentediting"],
            template: ` 
            <section class="main">
               <input v-model="toggleStat1" id="toggle-all" type="checkbox" class="toggle-all">
                <label for="toggle-all">Mark all as complete</label>
                <ul class="todo-list">
                        <li v-for="(item, index) in todos" 
                        v-bind:class="{completed: item.completed, editing: item === currentediting}">
                            <div class="view">
                                <input type="checkbox" class="toggle" v-model="item.completed">
                                <label @dblclick="doubleMethods(item)">{{item.title}}</label>
                                <button class="destroy" @click="removeTodoParent(index, $event)"></button>
                            </div>
                            <input class="edit" @keyup.enter="saveEdit(item, index, $event)" :value="item.title" @keyup.esc="currentediting = null">
                        </li>
                </ul>
            </section>
            `,
            methods: {
                //  3.1 删除操作
                //  3.2 把需要删除的id 传递给父组件过来
                removeTodoParent(index) {
                    this.$emit("removetodo", index)
                },
                saveEdit(item, index, event) {
                    var editText = event.target.value.trim()
                    this.$emit("save-edit", editText, item, index)
                },
                doubleMethods(item) {
                    this.$emit("dbl-click", item)
                }
            },
            computed: {
                toggleStat1: {
                    get() {
                        return this.todos.every(item => item.completed)
                    },
                    set(val) {
                        this.todos.forEach(todo => todo.completed = val)
                    }
                },
            },
        }


        var myfotter = {
            props: ['leftcount'],
            template: `
            <footer class="footer">
            <span class="todo-count"><strong>{{leftcount}}</strong> item left</span>
            <button class="clear-completed" @click="deleteAll">Clear completed</button>
        </footer>
            `,
            methods: {
                deleteAll() {
                    this.$emit("delete-all-done")
                }
            }
        }

        var vm = new Vue({
            el: "#todoapp",
            data: {
                currentEditing: null,
                todos: [{
                    id: 1,
                    title: '吃饭',
                    completed: false
                }, {
                    id: 2,
                    title: '睡觉',
                    completed: false
                }, {
                    id: 3,
                    title: '打豆豆',
                    completed: true
                }]
            },
            methods: {
                addTodo(todoText) {


                    const lastTodo = this.todos[this.todos.length - 1]
                    const id = lastTodo ? lastTodo.id + 1 : 1

                    // // 当数组发生变化,则绑定渲染该数组的视图也会得到更新
                    this.todos.push({
                        id,
                        title: todoText,
                        completed: false
                    })

                    // // 清空文本框
                    event.target.value = ''
                },
                // 删除任务项
                removeTodo(delIndex) {
                    this.todos.splice(delIndex, 1)
                },
                // 保存编辑项
                saveEdit(editText, item, index) {
                    console.log(item, index, '-------------')
                        // 1. 拿到文本框中的数据
                        //    非空校验
                        //    如果为空,则直接删除这个 item
                        //    如果不为空,则修改任务项的 title 数据


                    // 程序员要具有工匠精神:优化简写
                    // !editText.length ?
                    //   this.todos.splice(index, 1) :
                    //   item.title = editText

                    if (!editText.length) {
                        // 将元素直接从数组中移除
                        return this.todos.splice(index, 1)
                    }

                    // 2. 将数据设置到任务项中
                    item.title = editText

                    // 3. 去除 editing 样式
                    this.currentEditing = null
                },
                // 删除所有已完成任务项
                removeAllDone() {
                    // 找到所有已完成的任务项,把其删除。错误的写法
                    // this.todos.forEach((item, index) => {
                    //   if (item.completed) {
                    //     // 已完成
                    //     console.log(item.title)
                    //     this.todos.splice(index, 1)
                    //   }
                    // })

                    // 把所有需要保留的数据过滤出来,然后重新赋值给 todos
                    this.todos = this.todos.filter(item => !item.completed)

                    // 如果想要就在遍历的过程去删除,则可以使用 for 循环
                    // 没删除一个,我们可以控制让索引 --
                    // for (let i = 0; i < this.todos.length; i++) {
                    //   if (this.todos[i].completed) {
                    //     this.todos.splice(i, 1)
                    //     i--
                    //   }
                    // }
                },
                aaaa(item) {
                    console.log(123)
                    this.currentEditing = item
                }
            },
            computed: {

                leftCount: function() {
                    return this.todos.filter(item => !item.completed).length
                },

            },
            components: {
                myheader,
                mylist,
                myfotter
            }
        })
    </script>
</body>

</html>
hr {
	margin: 20px 0;
	border: 0;
	border-top: 1px dashed #c5c5c5;
	border-bottom: 1px dashed #f7f7f7;
}

.learn a {
	font-weight: normal;
	text-decoration: none;
	color: #b83f45;
}

.learn a:hover {
	text-decoration: underline;
	color: #787e7e;
}

.learn h3,
.learn h4,
.learn h5 {
	margin: 10px 0;
	font-weight: 500;
	line-height: 1.2;
	color: #000;
}

.learn h3 {
	font-size: 24px;
}

.learn h4 {
	font-size: 18px;
}

.learn h5 {
	margin-bottom: 0;
	font-size: 14px;
}

.learn ul {
	padding: 0;
	margin: 0 0 30px 25px;
}

.learn li {
	line-height: 20px;
}

.learn p {
	font-size: 15px;
	font-weight: 300;
	line-height: 1.3;
	margin-top: 0;
	margin-bottom: 0;
}

#issue-count {
	display: none;
}

.quote {
	border: none;
	margin: 20px 0 60px 0;
}

.quote p {
	font-style: italic;
}

.quote p:before {
	content: '“';
	font-size: 50px;
	opacity: .15;
	position: absolute;
	top: -20px;
	left: 3px;
}

.quote p:after {
	content: '”';
	font-size: 50px;
	opacity: .15;
	position: absolute;
	bottom: -42px;
	right: 3px;
}

.quote footer {
	position: absolute;
	bottom: -40px;
	right: 0;
}

.quote footer img {
	border-radius: 3px;
}

.quote footer a {
	margin-left: 5px;
	vertical-align: middle;
}

.speech-bubble {
	position: relative;
	padding: 10px;
	background: rgba(0, 0, 0, .04);
	border-radius: 5px;
}

.speech-bubble:after {
	content: '';
	position: absolute;
	top: 100%;
	right: 30px;
	border: 13px solid transparent;
	border-top-color: rgba(0, 0, 0, .04);
}

.learn-bar > .learn {
	position: absolute;
	width: 272px;
	top: 8px;
	left: -300px;
	padding: 10px;
	border-radius: 5px;
	background-color: rgba(255, 255, 255, .6);
	transition-property: left;
	transition-duration: 500ms;
}

@media (min-width: 899px) {
	.learn-bar {
		width: auto;
		padding-left: 300px;
	}

	.learn-bar > .learn {
		left: 8px;
	}
}
html,
body {
	margin: 0;
	padding: 0;
}

button {
	margin: 0;
	padding: 0;
	border: 0;
	background: none;
	font-size: 100%;
	vertical-align: baseline;
	font-family: inherit;
	font-weight: inherit;
	color: inherit;
	-webkit-appearance: none;
	appearance: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

body {
	font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
	line-height: 1.4em;
	background: #f5f5f5;
	color: #4d4d4d;
	min-width: 230px;
	max-width: 550px;
	margin: 0 auto;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	font-weight: 300;
}

:focus {
	outline: 0;
}

.hidden {
	display: none;
}

.todoapp {
	background: #fff;
	margin: 130px 0 40px 0;
	position: relative;
	box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
	            0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.todoapp input::-webkit-input-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp input::-moz-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp input::input-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp h1 {
	position: absolute;
	top: -155px;
	width: 100%;
	font-size: 100px;
	font-weight: 100;
	text-align: center;
	color: rgba(175, 47, 47, 0.15);
	-webkit-text-rendering: optimizeLegibility;
	-moz-text-rendering: optimizeLegibility;
	text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
	position: relative;
	margin: 0;
	width: 100%;
	font-size: 24px;
	font-family: inherit;
	font-weight: inherit;
	line-height: 1.4em;
	border: 0;
	color: inherit;
	padding: 6px;
	border: 1px solid #999;
	box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
	box-sizing: border-box;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.new-todo {
	padding: 16px 16px 16px 60px;
	border: none;
	background: rgba(0, 0, 0, 0.003);
	box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}

.main {
	position: relative;
	z-index: 2;
	border-top: 1px solid #e6e6e6;
}

.toggle-all {
	text-align: center;
	border: none; /* Mobile Safari */
	opacity: 0;
	position: absolute;
}

.toggle-all + label {
	width: 60px;
	height: 34px;
	font-size: 0;
	position: absolute;
	top: -52px;
	left: -13px;
	-webkit-transform: rotate(90deg);
	transform: rotate(90deg);
}

.toggle-all + label:before {
	content: '❯';
	font-size: 22px;
	color: #e6e6e6;
	padding: 10px 27px 10px 27px;
}

.toggle-all:checked + label:before {
	color: #737373;
}

.todo-list {
	margin: 0;
	padding: 0;
	list-style: none;
}

.todo-list li {
	position: relative;
	font-size: 24px;
	border-bottom: 1px solid #ededed;
}

.todo-list li:last-child {
	border-bottom: none;
}

.todo-list li.editing {
	border-bottom: none;
	padding: 0;
}

.todo-list li.editing .edit {
	display: block;
	width: 506px;
	padding: 12px 16px;
	margin: 0 0 0 43px;
}

.todo-list li.editing .view {
	display: none;
}

.todo-list li .toggle {
	text-align: center;
	width: 40px;
	/* auto, since non-WebKit browsers doesn't support input styling */
	height: auto;
	position: absolute;
	top: 0;
	bottom: 0;
	margin: auto 0;
	border: none; /* Mobile Safari */
	-webkit-appearance: none;
	appearance: none;
}

.todo-list li .toggle {
	opacity: 0;
}

.todo-list li .toggle + label {
	/*
		Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
		IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
	*/
	background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
	background-repeat: no-repeat;
	background-position: center left;
}

.todo-list li .toggle:checked + label {
	background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}

.todo-list li label {
	word-break: break-all;
	padding: 15px 15px 15px 60px;
	display: block;
	line-height: 1.2;
	transition: color 0.4s;
}

.todo-list li.completed label {
	color: #d9d9d9;
	text-decoration: line-through;
}

.todo-list li .destroy {
	display: none;
	position: absolute;
	top: 0;
	right: 10px;
	bottom: 0;
	width: 40px;
	height: 40px;
	margin: auto 0;
	font-size: 30px;
	color: #cc9a9a;
	margin-bottom: 11px;
	transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
	color: #af5b5e;
}

.todo-list li .destroy:after {
	content: '×';
}

.todo-list li:hover .destroy {
	display: block;
}

.todo-list li .edit {
	display: none;
}

.todo-list li.editing:last-child {
	margin-bottom: -1px;
}

.footer {
	color: #777;
	padding: 10px 15px;
	height: 20px;
	text-align: center;
	border-top: 1px solid #e6e6e6;
}

.footer:before {
	content: '';
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	height: 50px;
	overflow: hidden;
	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
	            0 8px 0 -3px #f6f6f6,
	            0 9px 1px -3px rgba(0, 0, 0, 0.2),
	            0 16px 0 -6px #f6f6f6,
	            0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
	float: left;
	text-align: left;
}

.todo-count strong {
	font-weight: 300;
}

.filters {
	margin: 0;
	padding: 0;
	list-style: none;
	position: absolute;
	right: 0;
	left: 0;
}

.filters li {
	display: inline;
}

.filters li a {
	color: inherit;
	margin: 3px;
	padding: 3px 7px;
	text-decoration: none;
	border: 1px solid transparent;
	border-radius: 3px;
}

.filters li a:hover {
	border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
	border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
	float: right;
	position: relative;
	line-height: 20px;
	text-decoration: none;
	cursor: pointer;
}

.clear-completed:hover {
	text-decoration: underline;
}

.info {
	margin: 65px auto 0;
	color: #bfbfbf;
	font-size: 10px;
	text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
	text-align: center;
}

.info p {
	line-height: 1;
}

.info a {
	color: inherit;
	text-decoration: none;
	font-weight: 400;
}

.info a:hover {
	text-decoration: underline;
}

/*
	Hack to remove background from Mobile Safari.
	Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
	.toggle-all,
	.todo-list li .toggle {
		background: none;
	}

	.todo-list li .toggle {
		height: 40px;
	}
}

@media (max-width: 430px) {
	.footer {
		height: 50px;
	}

	.filters {
		bottom: 10px;
	}
}

最后

以上就是糊涂水池为你收集整理的【15】Vue:03-组件、组件注册、Vue调试工具、Vue组件之间传值、组件插槽、购物车案例、todos案例、的全部内容,希望文章能够帮你解决【15】Vue:03-组件、组件注册、Vue调试工具、Vue组件之间传值、组件插槽、购物车案例、todos案例、所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(46)

评论列表共有 0 条评论

立即
投稿
返回
顶部