Vue2:组件高级(下)

Vue2:组件高级(下)

Date: May 25, 2023
Sum: 自定义指令、插槽、商品列表、动态组件


目标:

Untitled




自定义指令

基础概念:

概念

内置指令:vue 官方提供了 v-for、v-model、v-if 等常用的内置指令。

自定义指令:Vue支持让开发者,自己注册一些指令。这些指令被称为自定义指令。

自定义指令可以封装一些 dom 操作,扩展额外功能。

类型:私有自定义指令和全局自定义指令

语法:

指令中的配置项介绍:

inserted:当指令所绑定的元素,被添加到页面当中时,会自动调用

换句话说,就是被绑定元素插入父节点时调用的钩子函数

el:使用指令的那个DOM元素

局部注册:

在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。

示例代码如下:

//在Vue组件的配置项中
directives: {
  "指令名": {
    inserted () {
      // 可以对 el 标签,扩展额外功能
      el.focus()
    }
  }
}
directives: {
    //  自定义一个私有指令
    focus: {
      // 当被绑定的元素插入到 DOM 中时,自动触发 mounted 函数
      mounted(el) {
        el.focus() // 让被绑定的元素自动获得焦点
      }
    }
  },

注意:自定义指令在使用的时候以v-开头,但是在声明的时候不需要加v-前缀

全局注册:

//在main.js中
Vue.directive('指令名', {
  "inserted" (el) {
    // 可以对 el 标签,扩展额外功能
    el.focus()
  }
})

全局共享的自定义指令需要通过“单页面应用程序的实例对象”进行声明,示例代码如下:

const app = Vue.createApp({})

// 注册一个全局自定义指令 `v-focue`
app.directive('focus', {
	// 当被绑定的元素插入到 DOM 中时,自动触发 mounted 函数
	mounted(el) {
		// Focus the element
		el.focus()
	}
})

使用自定义指令

在使用自定义指令时,需要加上 v- 前缀。

示例代码如下:

<!-- 声明自定义指令时,指令的名字是 focus -->
<!-- 使用自定义指令时,需要加上 v- 指令前缀 -->
<input v-focus />

案例:当页面加载时,让元素获取焦点(autofocus在safari浏览器有兼容性

  • Code: 采用局部注册与全局注册两种方式

    App.vue

    <template>
      <div>
        <h1>自定义指令</h1>
        <input v-focus ref="inp" type="text">
      </div>
    </template>
    
    <script>
    export default {
      // mounted() {
      //   this.$refs.inp.focus()
      // }
    
      // 2. 局部注册指令
      directives: {
        // 指令:指令的配置项
        focus: {
          inserted(el) {
            el.focus()
          }
        }
      }
    }
    </script>
    
    <style>
    
    </style>
    

    main.js

    import Vue from 'vue'
    import App from './App.vue'
    
    Vue.config.productionTip = false
    
    // 全局注册指令
    Vue.directive('focus', {
      inserted(el) {
        el.focus()
      }
    })
    
    new Vue({
      render: h => h(App),
    }).$mount('#app')
    


自定义指令-指令的值

**需求:**实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色

语法:

1.在绑定指令时,可以通过“等号”的形式为指令 绑定 具体的参数值

<div v-color="color">我是内容</div>

2.通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数

directives: {
  color: {
    inserted (el, binding) {
      el.style.color = binding.value
    },
    update (el, binding) {
      el.style.color = binding.value
    }
  }
}

updated 函数

mounted或inserted 函数只在元素第一次插入 DOM 时被调用,当 DOM 更新时 mounted或inserted 函数不会被触发。 updated函数会在每次 DOM 更新完成后被调用。

示例代码如下:

app.directive('focus', {
	mounted(el) {  // 第一次插入 DOM 时触发这个函数
		el.focus()
	},
	updated(el) { // 每次 DOM 更新时都会触发 updated 函数
		el.focus()
	}
})

注意:在 vue2 的项目中使用自定义指令时,【 mounted -> bind 】【 updated -> update 】

函数简写

如果 mounted 和updated 函数中的逻辑完全相同,则可以简写成如下格式:

app.directive('focus', (el) => {
	// 在 mounted 和 updated 时都会触发相同的业务处理
	el.focus()
})

案例

Untitled

  • Code:

    App.vue

    <template>
      <div>
        <h1 v-color="color1">指令的值1测试</h1>
        <h1 v-color="color2">指令的值2测试</h1>
      </div>
    </template>
    
    <script>
    export default {
      data () {
        return {
          color1: 'red',
          color2: 'green',
        }
      },
      directives: {
        color: {
          inserted(el, binding) {
            el.style.color = binding.value
          },
          update(el, binding) {
            console.log('指令的值被修改!');
            el.style.color = binding.value
          }
        }
      }
    }
    </script>
    
    <style>
    
    </style>
    


自定义指令-v-loading指令的封装

场景:

开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好

**需求:**封装一个 v-loading 指令,实现加载中的效果

分析:

1.本质 loading效果就是一个蒙层,盖在了盒子上

2.数据请求中,开启loading状态,添加蒙层

3.数据请求完毕,关闭loading状态,移除蒙层

实现:

1.准备一个 loading类,通过伪元素定位,设置宽高,实现蒙层

2.开启关闭 loading状态(添加移除蒙层),本质只需要添加移除类即可

3.结合自定义指令的语法进行封装复用

.loading:before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: #fff url("./loading.gif") no-repeat center;
}

案例

Untitled

  • Code:

    App.vue

    <template>
      <div class="main">
        <div class="box" v-loading="isLoading">
          <ul>
            <li v-for="item in list" :key="item.id" class="news">
              <div class="left">
                <div class="title">{{ item.title }}</div>
                <div class="info">
                  <span>{{ item.source }}</span>
                  <span>{{ item.time }}</span>
                </div>
              </div>
    
              <div class="right">
                <img :src="item.img" alt="">
              </div>
            </li>
          </ul>
        </div>
      </div>
    </template>
    
    <script>
    // 安装axios =>  yarn add axios
    import axios from 'axios'
    
    // 接口地址:http://hmajax.itheima.net/api/news
    // 请求方式:get
    export default {
      data () {
        return {
          list: [],
          isLoading: true,
        }
      },
      async created () {
        // 1. 发送请求获取数据
        const res = await axios.get('http://hmajax.itheima.net/api/news')
        
        setTimeout(() => {
          // 2. 更新到 list 中
          this.list = res.data.data
          this.isLoading = false
        }, 2000)
      },
      directives: {
        loading: {
          inserted(el, binding) {
            binding.value ? el.classList.add('loading') : el.classList.remove('loading')
          },
          update(el, binding) {
            binding.value ? el.classList.add('loading') : el.classList.remove('loading')
          },
        }
      }
    }
    </script>
    
    <style>
    /* 伪类 - 蒙层效果 */
    .loading:before {
      content: '';
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background: #fff url('./loading.gif') no-repeat center;
    }
    
    /* .box2 {
      width: 400px;
      height: 400px;
      border: 2px solid #000;
      position: relative;
    } */
    
    .box {
      width: 800px;
      min-height: 500px;
      border: 3px solid orange;
      border-radius: 5px;
      position: relative;
    }
    .news {
      display: flex;
      height: 120px;
      width: 600px;
      margin: 0 auto;
      padding: 20px 0;
      cursor: pointer;
    }
    .news .left {
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding-right: 10px;
    }
    .news .left .title {
      font-size: 20px;
    }
    .news .left .info {
      color: #999999;
    }
    .news .left .info span {
      margin-right: 20px;
    }
    .news .right {
      width: 160px;
      height: 120px;
    }
    .news .right img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    </style>
    

总结

  1. 通过指令相关语法,封装了指令 v-loading 实现了请求的loading效果
  2. 核心思路:
    1. 准备类名 loading,通过伪元素提供遮罩层

    2. 添加或移除类名,实现loading蒙层的添加移除

    3. 利用指令语法,封装 v-loading 通用指令

      inserted 钩子中,binding.value 判断指令的值,设置默认状态
      update 钩子中,binding.value 判断指令的值,更新类名状态




插槽

概念

插槽(Slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。

Untitled

作用:让组件内部的一些 结构 支持 自定义

可以把插槽认为是组件封装期间,为用户预留的内容的占位符。

案例

将需要多次显示的对话框,封装成一个组件

Untitled

组件的内容部分,不希望写死,希望能使用的时候自定义。怎么办



体验插槽的基础用法

在封装组件时,可以通过 元素定义插槽,从而为用户预留内容占位符。

注意:给插槽传入内容时,可以传入纯文本、html标签、组件

示例代码如下:

MyCom.vue

<template>
    <p>这是第一个p标签</p>
    <!-- 1. 通过 slot 标签,为用户预留内容占位符(插槽) -->
    <slot></slot>
    <p>这是最后一个p标签</p>
</template>

App.vue

<!-- 使用组件 -->
<my-com>
	<!-- 2. 在使用MyCom1 组件时, 为插槽指定具体的内容--->
  <p>~~~用户自定义的内容~~~</p>
</my-com>

没有预留插槽的内容会被丢弃

如果在封装组件时没有预留任何 插槽,则用户提供的任何自定义内容都会被丢弃。

示例代码如下:

MyCom.vue

<template>
    <p>这是第一个p标签</p>
    <!-- 封装组件时,没有预留任何插槽 -->
    <p>这是最后一个p标签</p>
</template>

App.vue

<my-com-1>
	<!-- 自定义的内容会被丢弃 -->
	<p>~~~用户自定义的内容~~~<p>
</my-com-1>

默认内容
封装组件时,可以为预留的 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。

示例代码如下:

<template>
    <p>这是第一个p标签</p>
    <slot>这是后备内容</slot>
    <p>这是最后一个p标签</p>
</template>

案例:对话框复用

Untitled

  • Code:


具名插槽

需求:一个组件内有多处结构,需要外部传入标签,进行定制

Untitled

上面的弹框中有三处不同,但是默认插槽只能定制一个位置,这时候怎么办呢?

解决方案:如果在封装组件时需要预留多个插槽节点,则需要为每个 插槽指定具体的 name 名称。这种带有具体名称的插槽叫做“具名插槽”。

示例代码如下:

多个slot使用name属性区分名字

MyCom.vue

<template>
  <div>
    <header>
      <!-- 我们希望把页头放这里 -->
      <slot name="header"></slot>
    </header>
    
    <main>
      <!-- 我们希望把主要内容放这里 -->
      <slot></slot>
    </main>
    
    <footer>
      <!-- 我们希望把页脚放这里 -->
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

注意:没有指定 name 名称的插槽,会有隐含的名称叫做 “default”。

App.vue

为具名插槽提供内容

通过 元素上使用 v-slot 指令向具名插槽提供内容,并以 v-slot 的参数的形式提供其名称。即,template配合v-slot:名字来分发对应标签。

<template>
  <div>
    <h1>App 根组件</h1>
    <hr/>

    <!-- 使用组件 -->
    <my-com>
      <template v-slot:header>
        <h1>滕王阁序</h1>
      </template>

      <template v-slot:default>
        <p>test1</p>
        <p>test2</p>
        <p>test3</p>        
      </template>

      <template v-slot:footer>
        <p>落款:王勃</p>
      </template>
    </my-com>
  </div>
</template>

案例

Untitled

  • Code:

    App.vue

    <template>
      <div>
        <MyDialog>
          <template v-slot:content>
            <div>一段内容</div>
          </template>
          <template v-slot:footer>
            <button>取消</button>
            <button>确认</button>
          </template>
        </MyDialog>
      </div>
    </template>
    
    <script>
    import MyDialog from './components/MyDialog.vue'
    export default {
      data () {
        return {
    
        }
      },
      components: {
        MyDialog
      }
    }
    </script>
    
    <style>
    body {
      background-color: #b3b3b3;
    }
    </style>
    

    MyDialog.vue

    <template>
      <div class="dialog">
        <div class="dialog-header">
          <h3>友情提示</h3>
          <span class="close">✖️</span>
        </div>
    
        <div class="dialog-content">
          <!-- 1. 需要定制的位置,使用slot占位 -->
          <slot name="content"></slot>
        </div>
        <div class="dialog-footer">
          <slot name="footer"></slot>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data () {
        return {
    
        }
      }
    }
    </script>
    
    <style scoped>
    * {
      margin: 0;
      padding: 0;
    }
    .dialog {
      width: 470px;
      height: 230px;
      padding: 0 25px;
      background-color: #ffffff;
      margin: 40px auto;
      border-radius: 5px;
    }
    .dialog-header {
      height: 70px;
      line-height: 70px;
      font-size: 20px;
      border-bottom: 1px solid #ccc;
      position: relative;
    }
    .dialog-header .close {
      position: absolute;
      right: 0px;
      top: 0px;
      cursor: pointer;
    }
    .dialog-content {
      height: 80px;
      font-size: 18px;
      padding: 15px 0;
    }
    .dialog-footer {
      display: flex;
      justify-content: flex-end;
    }
    .dialog-footer button {
      width: 65px;
      height: 35px;
      background-color: #ffffff;
      border: 1px solid #e1e3e9;
      cursor: pointer;
      outline: none;
      margin-left: 10px;
      border-radius: 3px;
    }
    .dialog-footer button:last-child {
      background-color: #007acc;
      color: #fff;
    }
    </style>
    

具名插槽的简写形式

跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。

例如 v-slot:header可以被重写为 #header

App.vue

<template>
  <div>
    <h1>App 根组件</h1>
    <hr/>

    <!-- 使用组件 -->
    <my-com>
      <template #header>
        <h1>滕王阁序</h1>
      </template>

      <template #default>
        <p>test1</p>
        <p>test2</p>
        <p>test3</p>        
      </template>

      <template #footer>
        <p>落款:王勃</p>
      </template>
    </my-com>
  </div>
</template>


作用域插槽

概念:在封装组件的过程中,可以为预留的 插槽绑定 props 数据,这种带有 props 数据的 叫做“作用域插槽”。

分类

  • 默认插槽
  • 具名插槽 插槽只有两种,作用域插槽不属于插槽的一种分类

作用:定义slot 插槽的同时, 是可以传值的。给 插槽 上可以 绑定数据,将来 使用组件时可以用

案例

案例1:封装表格组件

Untitled

使用步骤:

1- 给 slot 标签, 以添加属性的方式传值

<slot :id="item.id" msg="测试文本"></slot>

2- 所有添加的属性, 都会被收集到一个对象中

{ id: 3, msg: '测试文本' }

3- 在template中, 通过 #插槽名= "obj" 接收,默认插槽名为 default

<MyTable :list="list">
  <template #default="obj">
    <button @click="del(obj.id)">删除</button>
  </template>
</MyTable>
  • Code:

    MyTable.vue

    <template>
      <table class="my-table">
        <thead>
          <tr>
            <th>序号</th>
            <th>姓名</th>
            <th>年纪</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(item, index) in data" :key="item.id">
            <td>{{ index + 1 }}</td>
            <td>{{ item.name }}</td>
            <td>{{ item.age }}</td>
            <td>
              <!-- 1. 给slot标签,添加属性的方式传值 -->
              <slot :row="item" msg="测试文本"></slot>
              <!-- 2. 所有的属性会添加到一个对象中 -->
              <!-- 
                {
                  id: 2,
                  msg: ‘测试文本’
                }
               -->
            </td>
          </tr>
        </tbody>
      </table>
    </template>
    
    <script>
    export default {
      props: {
        data: Array,
      },
    }
    </script>
    
    <style scoped>
    .my-table {
      width: 450px;
      text-align: center;
      border: 1px solid #ccc;
      font-size: 24px;
      margin: 30px auto;
    }
    .my-table thead {
      background-color: #1f74ff;
      color: #fff;
    }
    .my-table thead th {
      font-weight: normal;
    }
    .my-table thead tr {
      line-height: 40px;
    }
    .my-table th,
    .my-table td {
      border-bottom: 1px solid #ccc;
      border-right: 1px solid #ccc;
    }
    .my-table td:last-child {
      border-right: none;
    }
    .my-table tr:last-child td {
      border-bottom: none;
    }
    .my-table button {
      width: 65px;
      height: 35px;
      font-size: 18px;
      border: 1px solid #ccc;
      outline: none;
      border-radius: 3px;
      cursor: pointer;
      background-color: #ffffff;
      margin-left: 5px;
    }
    </style>
    

    App.vue

     <template>
      <div>
        <MyTable :data="list">
          <template #default="obj">
            <button @click="handleDel(obj.row.id)">删除</button>
          </template>
        </MyTable>
        <MyTable :data="list2">
          <!-- 采用解构的方式,解构obj出row -->
          <template #default="{ row }">
            <button  @click="handleAlert(row)">展示</button>
          </template>
        </MyTable>
      </div>
    </template>
    
    <script>
    import MyTable from './components/MyTable.vue'
    export default {
      data () {
        return {
          list: [
            { id: 1, name: '张小花', age: 18 },
            { id: 2, name: '孙大明', age: 19 },
            { id: 3, name: '刘德忠', age: 17 },
          ],
          list2: [
            { id: 1, name: '赵小云', age: 18 },
            { id: 2, name: '刘蓓蓓', age: 19 },
            { id: 3, name: '姜肖泰', age: 17 },
          ]
        }
      },
      components: {
        MyTable
      },
      methods: {
        handleDel(id) {
          this.list = this.list.filter(item => item.id !== id)
        },
        handleAlert(row) {
          console.log(row);
        }
      }
    }
    </script>
    

对于Code的用法的理解

解构作用域插槽的 Prop

作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程。

示例代码如下:

<MyTable :data="list2">
  <!-- 采用解构的方式,解构obj出row -->
  <template #default="{ row }">
    <button  @click="handleAlert(row)">展示</button>
  </template>
</MyTable>



综合案例:商品列表

需求说明:

效果

Untitled

需求说明:

  1. my-tag 标签组件封装

(1) 双击显示输入框,输入框获取焦点

(2) 失去焦点,隐藏输入框

(3) 回显标签信息

(4) 内容修改,回车 → 修改标签信息

  1. my-table 表格组件封装

(1) 动态传递表格数据渲染

(2) 表头支持用户自定义

(3) 主体支持用户自定义



MyTag组件封装

封装内容:MyTag

Untitled

完成步骤

(1) 双击显示输入框,输入框获取焦点

关键点:

1-显示输入框:v-if v-else @dbclick

2-自动聚焦: n e x t T i c k → nextTick→ nextTickrefs 获取dom 或者 自定义指令v-focus

(2) 失去焦点,隐藏输入框

关键点:

1-@blur=”isEdit = false”

  • Code:

    App.vue

    <template>
      <div class="table-case">
        <table class="my-table">
          <thead>
            <tr>
              <th>编号</th>
              <th>名称</th>
              <th>图片</th>
              <th width="100px">标签</th>
            </tr>
          </thead>
          <tbody>
            
            <tr>
              <td>1</td>
              <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td>
              <td>
                <img src="https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg" />
              </td>
              <td>
                <!-- 标签组件 -->
                <MyTag></MyTag>
              </td>
            </tr>
    
          </tbody>
        </table>
      </div>
    </template>
    
    <script>
    import MyTag from './components/MyTag.vue'
    export default {
      name: 'TableCase',
      components: {
        MyTag
      },
      data() {
        return {
          goods: [
            {
              id: 101,
              picture:
                'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg',
              name: '梨皮朱泥三绝清代小品壶经典款紫砂壶',
              tag: '茶具',
            },
            {
              id: 102,
              picture:
                'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg',
              name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌',
              tag: '男鞋',
            },
            {
              id: 103,
              picture:
                'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png',
              name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm',
              tag: '儿童服饰',
            },
            {
              id: 104,
              picture:
                'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg',
              name: '基础百搭,儿童套头针织毛衣1-9岁',
              tag: '儿童服饰',
            },
          ],
        }
      },
    }
    </script>
    
    <style lang="less" scoped>
    .table-case {
      width: 1000px;
      margin: 50px auto;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
    
      .my-table {
        width: 100%;
        border-spacing: 0;
        img {
          width: 100px;
          height: 100px;
          object-fit: contain;
          vertical-align: middle;
        }
        th {
          background: #f5f5f5;
          border-bottom: 2px solid #069;
        }
        td {
          border-bottom: 1px dashed #ccc;
        }
        td,
        th {
          text-align: center;
          padding: 10px;
          transition: all 0.5s;
          &.red {
            color: red;
          }
        }
        .none {
          height: 100px;
          line-height: 100px;
          color: #999;
        }
      }
    
    }
    </style>
    

    MyTag.vue

    <template>
      <div class="my-tag">
          <input v-if="isEdit"
            v-focus
            ref="inp"
            class="input"
            type="text"
            placeholder="输入标签"
            @blur="isEdit = false"
          />
          <div 
            @dblclick="handleClick"
            v-else
            class="text">
            茶具
          </div>
        </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          isEdit: false
        }
      },
      methods: {
        handleClick() {
          // 双击后,切换到显示状态
          this.isEdit = true
          // 立刻获取焦点 (由于Vue是异步Dom更新,所以这里需要采用$nextTick())
          // Plan1: $nextTick
          // this.$nextTick(() => {
          //   this.$refs.inp.focus()
          // })
          // Plan2: v-focus
        },
      }
    }
    </script>
    
    <style lang="less" scoped>
    .my-tag {
      cursor: pointer;
      .input {
        appearance: none;
        outline: none;
        border: 1px solid #ccc;
        width: 100px;
        height: 40px;
        box-sizing: border-box;
        padding: 10px;
        color: #666;
        &::placeholder {
          color: #666;
        }
      }
    }
    </style>
    

    main.js

    import Vue from 'vue'
    import App from './App.vue'
    
    Vue.config.productionTip = false
    
    // 全局注册指令
    Vue.directive('focus', {
      inserted(el) {
        el.focus()
      }
    })
    
    new Vue({
      render: h => h(App),
    }).$mount('#app')
    

(3) 回显标签信息

关键点:

1-回显的标签

回显的标签信息是父组件传递过来的

v-model实现功能(简化代码) v-model ⇒ :value 和 @input

(4) 内容修改,回车 → 修改标签信息

关键点:

1-enter子传父

子传父,将回车时,输入框的内容提交给父组件更新

由于父组件是v-model,触发事件,需要触发 input 事件

2-拿到文本框中实时的值:

e.target 指触发事件的事件源

  • Code:

    App.vue

    <template>
      <div class="table-case">
        <table class="my-table">
          <thead>
            <tr>
              <th>编号</th>
              <th>名称</th>
              <th>图片</th>
              <th width="100px">标签</th>
            </tr>
          </thead>
          <tbody>
            
            <tr>
              <td>1</td>
              <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td>
              <td>
                <img src="https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg" />
              </td>
              <td>
                <!-- 标签组件 -->
                <MyTag v-model="tempText"></MyTag>
              </td>
            </tr>
    
          </tbody>
        </table>
      </div>
    </template>
    
    <script>
    import MyTag from './components/MyTag.vue'
    export default {
      name: 'TableCase',
      components: {
        MyTag
      },
      data() {
        return {
          tempText: '紫砂壶',
          goods: [
            {
              id: 101,
              picture:
                'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg',
              name: '梨皮朱泥三绝清代小品壶经典款紫砂壶',
              tag: '茶具',
            },
            {
              id: 102,
              picture:
                'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg',
              name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌',
              tag: '男鞋',
            },
            {
              id: 103,
              picture:
                'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png',
              name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm',
              tag: '儿童服饰',
            },
            {
              id: 104,
              picture:
                'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg',
              name: '基础百搭,儿童套头针织毛衣1-9岁',
              tag: '儿童服饰',
            },
          ],
        }
      },
    }
    </script>
    
    <style lang="less" scoped>
    .table-case {
      width: 1000px;
      margin: 50px auto;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
    
      .my-table {
        width: 100%;
        border-spacing: 0;
        img {
          width: 100px;
          height: 100px;
          object-fit: contain;
          vertical-align: middle;
        }
        th {
          background: #f5f5f5;
          border-bottom: 2px solid #069;
        }
        td {
          border-bottom: 1px dashed #ccc;
        }
        td,
        th {
          text-align: center;
          padding: 10px;
          transition: all 0.5s;
          &.red {
            color: red;
          }
        }
        .none {
          height: 100px;
          line-height: 100px;
          color: #999;
        }
      }
    
    }
    </style>
    

    MyTag.vue

    <template>
      <div class="my-tag">
          <input 
            v-if="isEdit"
            v-focus
            ref="inp"
            class="input"
            type="text"
            :value="value"
            placeholder="输入标签"
            @blur="isEdit = false"
            @keyup.enter="handleEnter"
          />
          <div 
            @dblclick="handleClick"
            v-else
            class="text">
            {{ value }}
          </div>
        </div>
    </template>
    
    <script>
    export default {
      props: {
        value: String,
      },
      data() {
        return {
          isEdit: false
        }
      },
      methods: {
        handleClick() {
          // 双击后,切换到显示状态
          this.isEdit = true
          // 立刻获取焦点 (由于Vue是异步Dom更新,所以这里需要采用$nextTick())
          // Plan1: $nextTick
          // this.$nextTick(() => {
          //   this.$refs.inp.focus()
          // })
          // Plan2: v-focus
        },
        handleEnter(e) {
          // 1-enter子传父
          // 子传父,将回车时,输入框的内容提交给父组件更新
          // 由于父组件是v-model,触发事件,需要触发 input 事件
          
          // 2-拿到文本框中实时的值:
          // e.target 指触发事件的事件源
          if(e.target.value.trim() === ''){
            alert('请输入内容!')
            return
          }
          this.$emit('input', e.target.value)
          this.isEdit = false
        },
      }
    }
    </script>
    
    <style lang="less" scoped>
    .my-tag {
      cursor: pointer;
      .input {
        appearance: none;
        outline: none;
        border: 1px solid #ccc;
        width: 100px;
        height: 40px;
        box-sizing: border-box;
        padding: 10px;
        color: #666;
        &::placeholder {
          color: #666;
        }
      }
    }
    </style>
    


MyTable组件封装

(1) 动态传递表格数据渲染

(2) 表头支持用户自定义

(3) 主体支持用户自定义

关键:插槽(具名插槽、作用域插槽)

  • Code:

    App.vue

    <template>
      <div class="table-case">
        <MyTable :data="goods">
          <template #head>
            <th>编号</th>
            <th>名称</th>
            <th>图片</th>
            <th width="100px">标签</th>
          </template>
          <template #body="{ index, item }">
            <td>{{ index }}</td>
            <td>{{ item.name }}</td>
            <td>
              <img :src=item.picture />
            </td>
            <td>
              <!-- 标签组件 -->
              <MyTag v-model="item.tag"></MyTag>
            </td>
          </template>
        </MyTable>
      </div>
    </template>
    
    <script>
    // my-table 表格组件的封装
    // 1.数据不能写死,动态传递表格渲染的数据 props
    // 2.结构不能写死 -多处结构自定义 【具名插槽】
    // (1)表头支持自定义
    // (2)主体支持自定义
    
    import MyTag from './components/MyTag.vue'
    import MyTable from './components/MyTable.vue'
    export default {
      name: 'TableCase',
      components: {
        MyTag,
        MyTable
      },
      data() {
        return {
          goods: [
            {
              id: 101,
              picture:
                'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg',
              name: '梨皮朱泥三绝清代小品壶经典款紫砂壶',
              tag: '茶具',
            },
            {
              id: 102,
              picture:
                'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg',
              name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌',
              tag: '男鞋',
            },
            {
              id: 103,
              picture:
                'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png',
              name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm',
              tag: '儿童服饰',
            },
            {
              id: 104,
              picture:
                'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg',
              name: '基础百搭,儿童套头针织毛衣1-9岁',
              tag: '儿童服饰',
            },
          ],
        }
      },
    }
    </script>
    
    <style lang="less" scoped>
    .table-case {
      width: 1000px;
      margin: 50px auto;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
    }
    </style>
    

    MyTable.vue

    <template>
      <table class="my-table">
        <thead>
          <tr>
            <slot name="head"></slot>
     
          </tr>
        </thead>
        
        <tbody>
          <tr v-for="(item, index) in data" :key="item.id">
            <!-- 使用作用域插槽 -->
            <slot name="body" :item="item" :index="index"></slot>
          </tr>
        </tbody>
    
      </table>
    </template>
    
    <script>
    export default {
      props: {
        data: {
          type: Array,
          required: true,
        }
      },
    }
    </script>
    
    <style lang="less" scoped>
    .my-table {
      width: 100%;
      border-spacing: 0;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
      th {
        background: #f5f5f5;
        border-bottom: 2px solid #069;
      }
      td {
        border-bottom: 1px dashed #ccc;
      }
      td,
      th {
        text-align: center;
        padding: 10px;
        transition: all 0.5s;
        &.red {
          color: red;
        }
      }
      .none {
        height: 100px;
        line-height: 100px;
        color: #999;
      }
    }
    </style>
    

    MyTag.vue同上




动态组件

  1. 什么是动态组件

动态组件指的是动态切换组件的显示与隐藏。vue 提供了一个内置的 组件,专门用来实现组件的动态渲染。

① 是组件的占位符

② 通过 is 属性动态指定要渲染的组件名称

  1. 如何实现动态组件渲染

示例代码如下:

data() {
  return {
    comName: 'MyHome' // 1.当前要渲染的组件的名称
  }
},

<template>
  <div>
    <h1 class="mb-4">App 根组件</h1>
		<!-- 点击按钮,动态切换组件的名称 -->
    <button @click="comName = 'MyHome'">首页</button>
    <button @click="comName = 'MyMovie'">电影</button>
    <hr />
    <!-- 2.用is来指定需要渲染的组件的名字 -->
    <component :is="comName"></component>
  </div>
</template>

注意:当我们切换组件时,之前组件会被销毁(所以之前的数据也会被清空)

Untitled

  1. 使用 keep-alive 保持状态

默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的 组件保持动态组件的状态。

示例代码如下:

<keep-alive>
  <component :is="comName"></component>
</keep-alive>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/66553.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python练习 linux系统资源监控

yum install python3 yum -y install python3-pip yum -y install epel-release yum -y install gcc python-devel pip install --trusted-host pypi.tuna.tsinghua.edu.cn psutil 初版&#xff1a; import os import sys import time import platform import subprocess …

day10 快速排序 方法重载 和 方法递推

方法重载 斐波拉契数列问题 使用重载思想解决 public static int method(int n){if (n 2 ){return 1 ;}return (n-1)*2method(n-1);}public static int f(int n){if (n 1){return 1;}if (n 2){return 2;}return f(n-1)f(n-2);} 快速排序 思维很简单&#xff0c;类似二…

Oracle以逗号分隔的字符串拆分为多行数据实例详解

前言 近期在工作中遇到某表某字段是可扩展数据内容&#xff0c;信息以逗号分隔生成的&#xff0c;现需求要根据此字段数据在其它表查询相关的内容展现出来&#xff0c;第一想法是切割数据&#xff0c;以逗号作为切割符&#xff0c;以下为总结的实现方法&#xff0c;以供大家参…

python制作小程序制作流程,用python编写一个小程序

这篇文章主要介绍了python制作小程序代码宠物运输&#xff0c;具有一定借鉴价值&#xff0c;需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获&#xff0c;下面让小编带着大家一起了解一下。 1 importtkinter2 importtkinter.messagebox3 importmath4 classJSQ:5 6 7 d…

第二季“数字强市建设体验团”活动感悟-张继群

目录 第二季“数字强市建设体验团”活动感悟-张继群 第二季“数字强市建设体验团”活动感悟-张继群 此次参观中山东安信木业、费县智慧城市运营中心、澳柯玛以及沂南双创科技园区给我留下深刻印象&#xff0c;我深深地感受到了现代科技与生活的紧密联系。我们临沂市智慧大数据…

在x86下运行的Ubuntu系统上部署QEMU用于模拟RISC-V硬件系统

1.配置工作环境 sudo apt install gcc bison flex libncurses-dev ninja-build \pkg-config build-essential zlib1g-dev pkg-config libglib2.0-dev \binutils-dev libboost-all-dev autoconf libtool libssl-dev \libpixman-1-dev python-capstone virtualenv software-prop…

selenium获取b站视频标题

一、下载selenium 1. 下载对应版本的浏览器驱动 2. 安装selenium 3.把浏览器驱动放到使用的python内核的script目录中 二、测试效果模拟登录b站 from selenium import webdriver from selenium.webdriver.common.by import By import timebrowser webdriver.Chrome() # 打…

设计模式之Bridge模式的C++实现

目录 1、Bridge模式的提出 2、Bridge模式的定义 3、Bridge模式总结 4、需求描述 5、多继承方式实现 6、使用Bridge设计模式实现 1、Bridge模式的提出 在软件功能模块设计中&#xff0c;如果类的实现功能划分不清晰&#xff0c;使得继承得到的子类往往是随着需求的变化&am…

Win10语言设置 - 显示语言和应用语言

前言 Win10的语言设置可以设置显示语言和应用语言。其中&#xff0c;显示语言用于显示系统文字&#xff1b;应用语言用于应用程序显示文字。下文介绍如何设置。 显示语言 打开系统设置&#xff0c;选择时间和语言&#xff0c;如下图&#xff1a; 修改Windows显示语言即可更…

搭建Django+pyhon+vue自动化测试平台

Django安装 使用管理员身份运行pycharm使用local 1 pip install django -i https://pypi.tuna.tsinghua.edu.cn/simple 检查django是否安装成功 1 python -m django --version 创建项目 1 1 django-admin startproject test cd 切换至创建的项目中启动django项目…

linux文本三剑客---grep,sed,awk

目录 grep 什么是grep&#xff1f; grep实例演示 命令参数&#xff1a; 案例演示&#xff1a; sed 概念&#xff1a; 常用选项&#xff1a; 案例演示&#xff1a; awk 概念&#xff1a; awk常用命令选项&#xff1a; awk变量&#xff1a; 内置变量 自定义变量 a…

Java课题笔记~ JavaWeb概述/开发基础

JavaWeb概述/开发基础 1.XML基础 &#xff08;1&#xff09;XML概述 &#xff08;2&#xff09;XML语法 &#xff08;3&#xff09;DTD约束 &#xff08;4&#xff09;Schema约束&#xff08;XML Schema 比 DTD 更强大&#xff09; 2.Web基础知识 Web是一个分布式的超媒…

1、如何实现两台电脑之间数据相互读写

一、确保两台电脑在同一个局域网中&#xff0c;可以使用网线【动态配置】进行两台电脑互连。 二、静态配置: 将IP地址和网关设为192.168.0.1&#xff0c;目的是让这台电脑做另一台电脑的网关&#xff0c;子网掩码一点击会自动添加。第二台电脑同样打开设置&#xff0c;此处IP地…

MySQL之深入InnoDB存储引擎——Undo页

文章目录 一、UNDO日志格式1、INSERT操作对应的UNDO日志2、DELETE操作对应的undo日志3、UPDATE操作对应的undo日志1&#xff09;不更新主键2&#xff09;更新主键的操作 3、增删改操作对二级索引的影响 二、UNDO页三、UNDO页面链表四、undo日志具体写入过程五、回滚段1、回滚段…

初中信息技术考试编程题,初中信息技术python教案

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;初中信息技术python编程题库 网盘&#xff0c;初中信息技术python编程教学&#xff0c;今天让我们一起来看看吧&#xff01; ID:12450455 资源大小&#xff1a;934KB 资料简介: 2019-2020学年初中信息技术【轻松备课】P…

阔别三年,领先回归!别克LPGA锦标赛申城十月再启高球盛会

2023年8月4日——2023年金秋十月&#xff0c;阔别中国赛场已久的别克LPGA锦标赛将强势归来&#xff0c;于10月12日至15日在上海旗忠花园高尔夫俱乐部再次拉开帷幕。作为三年来首个回归、同时也是今年国内唯一开赛的国际顶级高尔夫职业赛事&#xff0c;别克LPGA锦标赛将吸引全世…

vscode 设置滑条颜色

1. 默认的滑条是灰黑色的&#xff0c;很难看的清 2. 左下角&#xff0c;打开VS Code 设置功能 3. 输入命令 workbench color&#xff0c;回车 4. 找到工作台&#xff1a;自定义颜色设置&#xff0c;打开设置文件 setting.json 5. 打开配置文件 6. 添加颜色配置 "workben…

谷歌推出AI模型机器人RT2 将文本和图像输出为机器人动作

去年年底&#xff0c;ChatGPT火遍全球&#xff0c;全世界都见识了大语言模型的强大力量。人们对大模型不再陌生&#xff0c;开始使用基于大模型的应用绘画、作图、搜索资料、设计剧情等&#xff0c;而妙用不止于此。谷歌推出了Robotics Transformer 2(RT2)&#xff0c;这是一个…

JSON.stringify循环引用问题

前端使用到对象的深度复制通常会简单的使用JSON.parse(JSON.stringify(obj))实现 &#xff08;浅表复制会用Array.from、Object.assign、Object.create静态方法实现&#xff09;&#xff0c;但在对象存在循环引用的情况下&#xff08;比如&#xff1a;树结构中子对象存在parent…

API接口:企业信息核验

企业信息核验是现代企业管理中必不可少的一项业务&#xff0c;它可以帮助企业做出正确的决策。在这篇文章里&#xff0c;我们将会介绍如何使用API接口来对企业信息进行核验&#xff0c;并实现快捷、准确的查询。 一、API接口 在这里我们使用的是挖数据提供的企业信息核验API接…