Vue Day5【综合案例 - 插槽】商品列表(vue2的最后一个案例)

image

小结:

1. 商品列表的实现封装了 2 个组件:标签组件 和 表格组件

2. 封装用到的核心技术点:

(1)props父传子 $emit子传父 v-model

(2)$nextTick 自定义指令

(3)插槽:具名插槽,作用域插槽

具体代码:

0. main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// 封装全局指令 focus
// inserted是生命周期钩子函数,本身就是异步执行的
Vue.directive('focus', {
  // 指令所在的dom元素,被插入到页面中时触发
  inserted(el) {
    el.focus()
  }
})

new Vue({
  render: h => h(App),
}).$mount('#app')

1. 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="{ item, index}">
        <td>{{ index + 1 }}</td>
        <td><img :src="item.picture" /></td>
        <td>{{ item.name }}</td>
        <td>
          <!-- 标签组件 -->
          <MyTag v-model="item.tag"></MyTag>
        </td>
      </template>
    </MyTable>
  </div>
</template>

<script>
// my-tag 标签组件的封装
// 1. 创建组件 - 初始化
// 2. 实现功能
//    (1) 双击显示,并且自动聚焦
//        v-if v-else @dblclick 操作 isEdit
//        自动聚焦:
//        1. $nextTick => $refs 获取到dom,进行focus获取焦点
//        2. 封装v-focus指令
//    (2) 失去焦点,隐藏输入框
//        @blur 操作 isEdit 即可

//    (3) 回显标签信息
//        回显的标签信息是父组件传递过来的
//        v-model实现功能(简化代码) v-model => :value 和 @input
//        组件内部通过props接收,:value设置给输入框
//    (4) 内容修改了,回车 => 修改标签信息
//        @keyup.enter,触发事件 $emit('input', e.target.value)

//  ------------------------------------------------------------------------

// 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 {
      // 测试组件功能的临时数据
      tempText: '1水杯',
      tempText2: '2花瓶',
      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段代码
<style lang="less" scoped>
.table-case {
  width: 1000px;
  margin: 50px auto;
  img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
  }
}
</style>

2. 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段代码
<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 .5s;
    &.red {
      color: red;
    }
  }
  .none {
    height: 100px;
    line-height: 100px;
    color: #999;
  }
}
</style>

3. MyTag.vue

<template>
  <div class="my-tag">
    <input
      v-if="isEdit"
      v-focus
      ref="inp"
      class="input"
      type="text"
      placeholder="输入标签"
      :value="value"
      @blur="isEdit = false"
      @keyup.enter="handleEnter"
    />
    <div
     v-else
     @dblclick="handleClick"
     class="text">
     {{ value }}</div>
  </div>
</template>

<script>
/* 自动聚焦:等dom更新完了,再聚焦
 * 
 * 聚焦方法1:dom异步更新
 *   用this.$nextTick(函数体),等dom更新后,再触发执行此方法里的函数体
 * 聚焦方法2:自定义指令
 *   封装到指令里,就不用写等待更新了,因为inserted是生命周期钩子函数,本身就是异步执行的
 *   具体的代码为input标签里的v-focus
 */
export default {
  props:{
    value: String
  },
  data(){
    return {
      isEdit : false
    }
  },
  methods:{
    handleClick(){
      // 双击后,切换到显示状态(Vue是dom异步更新)
      this.isEdit = true
      
      // this.$nextTick(() => {
      //   this.$refs.inp.focus()
      // })     
    },
    handleEnter(e){
      // 非空处理
      if(e.target.value.trim() === '') return alert('标签内容不能为空')

      // 子传父,将回车时,[输入框的内容]提交给父组件更新
      // 由于父组件是v-model,触发事件,需要触发input事件
      this.$emit('input', e.target.value)
      // 提交完成,关闭输入状态
      this.isEdit = false
    }
  }

}
</script>
style段代码
<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>
posted @ 2025-09-24 23:19  岑素月  阅读(12)  评论(0)    收藏  举报