商品案例-组件封装(vue)

MyTable.vue

javascript 复制代码
<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>
export default {
  props: {
    value: String
  },
  data () {
    return {
      isEdit: false
    }
  },
  methods: {
    handleClick () {
      // 双击后,切换到显示状态 (Vue是异步dom更新)
      this.isEdit = true
      
      // // 等dom更新完了,再获取焦点
      // 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 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>

// my-table 表格组件的封装

// 1. 数据不能写死,动态传递表格渲染的数据 props

// 2. 结构不能写死 - 多处结构自定义 【具名插槽】

// (1) 表头支持自定义

// (2) 主体支持自定义

MyTag.vue

javascript 复制代码
<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>
export default {
  props: {
    value: String
  },
  data () {
    return {
      isEdit: false
    }
  },
  methods: {
    handleClick () {
      // 双击后,切换到显示状态 (Vue是异步dom更新)
      this.isEdit = true
      
      // // 等dom更新完了,再获取焦点
      // 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 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>

// my-tag 标签组件的封装

// 1. 创建组件 - 初始化

// 2. 实现功能

// (1) 双击显示,并且自动聚焦

// v-if v-else @dbclick 操作 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)

App.vue

javascript 复制代码
<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>{{ item.name }}</td>
        <td>
          <img
            :src="item.picture"
          />
        </td>
        <td>
          <MyTag v-model="item.tag"></MyTag>
        </td>
      </template>
    </MyTable>
  </div>
</template>

<script>

import MyTag from './components/MyTag.vue'
import MyTable from './components/MyTable.vue'
export default {
  name: 'TableCase',
  components: { 
    MyTag,
    MyTable
  },
  data () {
    return {
      // 测试组件功能的临时数据
      tempText: '水杯',
      tempText2: '钢笔',
      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>

一、各组件作用分析

1. MyTable 组件(通用表格组件)

核心作用:提供一个结构灵活的表格容器,支持表头和表格内容的自定义,数据由父组件动态传入。

  • 模板部分

    • 定义了表格的基础结构(tabletheadtbody)。
    • 通过具名插槽 实现结构自定义:
      • slot name="head":留给父组件定义表头(th)内容。
      • slot name="body":留给父组件定义表格行(td)内容,同时通过插槽 Props(itemindex)传递当前行数据和索引。
    • v-for="(item, index) in data":根据父组件传入的data数组循环渲染表格行。
  • 脚本部分

    • 通过props接收data(必填数组),作为表格的渲染数据源。
  • 样式部分

    • 定义表格的基础样式(宽度、边框、对齐方式等),包括表头(th)、单元格(td)、图片的默认样式,以及状态类(如.red.none)。

2. MyTag 组件(可编辑标签组件)

核心作用 :实现一个支持双击编辑、回车 / 失焦保存的标签组件,通过v-model与父组件同步数据。

  • 模板部分

    • 条件渲染:isEdittrue时显示输入框(input),否则显示文本(div)。
    • 输入框(input):
      • v-focus:自定义指令(用于自动聚焦,需额外定义)。
      • :value="value":绑定父组件传入的标签值。
      • @blur:失焦时退出编辑模式(isEdit = false)。
      • @keyup.enter:回车时触发保存逻辑(handleEnter)。
    • 文本区(div):
      • @dblclick:双击时进入编辑模式(isEdit = true)。
      • 显示value(父组件传入的标签值)。
  • 脚本部分

    • props: { value }:接收父组件通过v-model传入的标签文本。
    • data() { isEdit }:控制编辑 / 显示状态的开关。
    • 方法:
      • handleClick:双击时切换到编辑模式(需配合$nextTick确保输入框渲染后聚焦,原代码注释了该逻辑,实际需补充)。
      • handleEnter:回车时验证非空,通过$emit('input', 新值)通知父组件更新(配合v-model实现双向绑定),然后退出编辑模式。
  • 样式部分

    • 定义输入框和文本区的基础样式(边框、尺寸、光标等)。

3. App 组件(页面级组件)

核心作用 :整合MyTableMyTag组件,实现一个带可编辑标签的商品表格页面。

  • 模板部分

    • 使用MyTable组件,通过:data="goods"传入商品数据。
    • 自定义MyTable的表头(#head插槽):定义 "编号、名称、图片、标签" 四列。
    • 自定义MyTable的行内容(#body插槽):
      • 接收MyTable传递的item(当前商品)和index(索引)。
      • 依次渲染:编号(index + 1)、商品名称(item.name)、商品图片(img :src="item.picture")、可编辑标签(MyTag v-model="item.tag")。
  • 脚本部分

    • 导入并注册MyTableMyTag组件。
    • data()中定义goods数组:存储商品数据(包含idpicturenametag等字段),作为表格的数据源。
  • 样式部分

    • 定义页面容器样式(宽度、居中)和图片的补充样式。

二、运行流程

初始化渲染

  • TableCase组件挂载,渲染MyTable组件,并将goods数组传给MyTabledata props。
  • MyTable渲染表头:使用#head插槽的内容(四个th)。
  • MyTable循环goods数组,为每个商品渲染一行(tr),并通过#body插槽渲染行内容:
    • 编号、名称、图片按商品数据直接展示。
    • 标签列渲染MyTag组件,通过v-model="item.tag"绑定当前商品的tag值,初始显示item.tag的文本。

标签编辑交互:

  • 双击MyTag的文本区(div):触发handleClickisEdit变为true,切换到输入框(input)显示。
  • 输入框自动聚焦(需v-focus指令或$nextTick逻辑支持),用户修改内容后:
    • 按回车:触发handleEnter,验证非空后,通过$emit('input', 新值)通知App,item.tag被更新为新值,随后isEdit变为false,回到文本显示模式。
    • 失焦(点击其他区域):触发@blurisEdit变为false,退出编辑模式(未保存则保留原内容)。

数据同步

  • MyTag通过v-model与App的item.tag双向绑定,编辑后的标签值会实时更新到goods数组中,表格显示随之刷新。

补充

一. MyTag 组件中,value 的值是如何传来的

1. 父组件中通过 v-model 绑定数据

在使用 MyTag 组件的父组件(比如你之前案例中的 TableCase)中,通常会用 v-model 绑定一个数据,例如:

javascript 复制代码
<!-- 父组件 TableCase 中使用 MyTag -->
<MyTag v-model="item.tag"></MyTag>

这里的 item.tag 是父组件中的数据(比如商品的标签值,如 "茶具""男鞋" 等)。

2. v-model 的本质:value props + input 事件

Vue 中 v-model 是一个语法糖,它等价于两个操作:

  • 给子组件传递一个名为 value 的 props,值为绑定的数据(即 item.tag);
  • 监听子组件触发的 input 事件,当事件触发时,更新绑定的数据(item.tag)。

所以上面的 v-model 写法等价于:

javascript 复制代码
<!-- 等价于 v-model 的手动写法 -->
<MyTag 
  :value="item.tag"  <!-- 父传子:将 item.tag 作为 value 传给子组件 -->
  @input="newValue => item.tag = newValue"  <!-- 子传父:监听 input 事件更新数据 -->
></MyTag>
3. 子组件 MyTag 通过 props 接收 value

MyTag 组件中,通过 props 声明接收 value,就可以拿到父组件传递过来的值:

javascript 复制代码
// MyTag 组件的 props 定义
export default {
  props: {
    value: {  // 接收父组件通过 v-model 传递的 value
      type: String,
      default: ''  // 默认值为空字符串
    }
  },
  // ...
}

此时,MyTag 组件内部就可以通过 this.value 访问到父组件传递过来的标签值(比如 "茶具")。

4. MyTag 组件中使用 value

MyTag 的模板中,value 被用于两种场景:

  • 文本显示:当不处于编辑模式时(isEdit=false),div 中显示 {``{ value }},即父组件传递的标签值;
  • 输入框回显:当处于编辑模式时(isEdit=true),input 通过 :value="value" 绑定,显示当前标签值(方便用户在原有值基础上编辑)。

二、自动获取焦点功能实现

1.局部实现
javascript 复制代码
     this.$nextTick(() => {
        // 立刻获取焦点
        this.$refs.inp.focus()

在 Vue 中,this.$nextTick() 的作用是等待下一次 DOM 更新循环结束后执行回调函数 。在你的场景中,获取输入框焦点必须用 $nextTick,核心原因是:Vue 对 DOM 的更新是异步的

当你双击标签触发 handleClick 时,执行了 this.isEdit = true,这个操作会导致模板中的 v-if="isEdit" 条件变化(从 false 变为 true),进而触发输入框(input)的渲染(从隐藏到显示)。

但是,Vue 并不会在你修改 isEdit立即更新 DOM (比如立刻创建 input 元素)。Vue 会将数据变化缓存起来,等当前同步代码执行完后,再批量更新 DOM(这是 Vue 的性能优化策略,避免频繁操作 DOM 导致的性能损耗)。

也就是说:

  • 执行 this.isEdit = true 后,input 还未被实际渲染到 DOM 中;
  • 此时直接调用 this.$refs.inp.focus(),会因为 input 还不存在(this.$refs.inpundefined)而报错,或者无法成功聚焦。

this.$nextTick(() => { ... }) 会把回调函数推迟到下一次 DOM 更新循环结束后执行。此时:

  • Vue 已经完成了 isEdit = true 导致的 DOM 更新(input 已被成功渲染到页面中);
  • this.$refs.inp 能够正确获取到渲染后的 input 元素,调用 focus() 就能正常生效。

因为修改 isEdit 后,input 的渲染是异步的,$nextTick 确保了我们在 DOM 真正更新完成(input 已存在)之后 再执行聚焦操作,否则会因为元素不存在而失败。

2.全局实现
javascript 复制代码
Vue.directive('focus',{
  inserted(el){

    el.focus()
  }
})

在main.js文件中实现,不改变其他代码,添加如上代码

注意:需要在组件中的input标签追加 v-focus

javascript 复制代码
  <input
        v-if="isEdit" 
        v-focus
        ref="inp"
        class="input"
        type="text"
        :value="value"
        placeholder="输入标签"
        @blur="isEdit=false"
        @keyup.enter="shuru"
    />

三、handleEnter函数说明

1. 为什么需要传参数 e

handleEnter 是绑定在 input 元素的 @keyup.enter 事件上的处理函数(@keyup.enter="handleEnter")。在 Vue 中,当原生 DOM 事件(如 keyupclick 等)触发时,Vue 会自动将 原生事件对象(Event) 作为参数传递给事件处理函数。

这个事件对象 e 包含了当前事件的所有信息,比如:

  • e.target:触发事件的 DOM 元素(这里就是输入框 input);
  • e.type:事件类型(这里是 keyup);
  • e.key:触发事件的按键(这里是 Enter)。

通过 e,我们可以直接获取到触发事件的元素及其属性(如输入框的值),而不需要额外通过 ref 手动查找元素,这是事件处理的自然逻辑。

2. 为什么用 e.target.value 而不是 this.$refs.inp.value

两者在当前场景下理论上都能获取到输入框的值 ,但 e.target.value 更符合事件处理的逻辑,且更稳健,原因如下:

  • 直接性:e.target 就是当前触发事件的元素 @keyup.enter 事件是绑定在 input 上的,所以 e.target 必然指向这个 input 元素(事件的源头)。通过 e.target.value 可以直接拿到该元素的当前值,无需额外依赖其他标识(如 ref)。

  • 减少对 ref 的依赖,降低耦合 this.$refs.inp 依赖于模板中 ref="inp" 的定义。如果未来修改了 ref 的名称(比如改成 ref="input"),this.$refs.inp 就会失效,需要同步修改代码;而 e.target 完全不依赖 ref,只要事件绑定在 input 上,就始终能正确获取元素。

  • 符合原生事件处理的逻辑 在原生 JavaScript 中,处理输入框事件时,通常也是通过事件对象 e 获取目标元素的值(e.target.value),这是更通用的做法,符合开发者的直觉。

总结

  • 传参数 e 是因为 Vue 会自动传递原生事件对象,通过 e 可以便捷地获取事件相关信息(包括触发事件的元素)。
  • 使用 e.target.valuethis.$refs.inp.value 更直接、更稳健,减少了对 ref 的依赖,符合事件驱动的编程逻辑。

两种方式在当前场景下效果一致,但 e.target.value 是更推荐的写法。

相关推荐
不说别的就是很菜2 小时前
【前端面试】Vue篇
前端·vue.js·面试
IT_陈寒2 小时前
Java 17实战:我从老旧Spring项目迁移中总结的7个关键避坑点
前端·人工智能·后端
倚肆2 小时前
CSS 动画与变换属性详解
前端·css
blackorbird2 小时前
谷歌 Chrome 浏览器的指纹识别技术,一边反追踪一边搞追踪
前端·chrome
Mintopia3 小时前
🚀 共绩算力:3分钟拥有自己的图像优化服务-CodeFormer:先进的图像算法优化、修复马赛克、提升图片清晰度等
前端·人工智能·ai编程
Lhuu(重开版3 小时前
html语法
前端·html
月弦笙音3 小时前
【vue3】这些不常用的API,却很实用
前端·vue.js·面试