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 组件(通用表格组件)
核心作用:提供一个结构灵活的表格容器,支持表头和表格内容的自定义,数据由父组件动态传入。
-
模板部分:
- 定义了表格的基础结构(
table、thead、tbody)。 - 通过具名插槽 实现结构自定义:
slot name="head":留给父组件定义表头(th)内容。slot name="body":留给父组件定义表格行(td)内容,同时通过插槽 Props(item、index)传递当前行数据和索引。
v-for="(item, index) in data":根据父组件传入的data数组循环渲染表格行。
- 定义了表格的基础结构(
-
脚本部分:
- 通过
props接收data(必填数组),作为表格的渲染数据源。
- 通过
-
样式部分:
- 定义表格的基础样式(宽度、边框、对齐方式等),包括表头(
th)、单元格(td)、图片的默认样式,以及状态类(如.red、.none)。
- 定义表格的基础样式(宽度、边框、对齐方式等),包括表头(
2. MyTag 组件(可编辑标签组件)
核心作用 :实现一个支持双击编辑、回车 / 失焦保存的标签组件,通过v-model与父组件同步数据。
-
模板部分:
- 条件渲染:
isEdit为true时显示输入框(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 组件(页面级组件)
核心作用 :整合MyTable和MyTag组件,实现一个带可编辑标签的商品表格页面。
-
模板部分:
- 使用
MyTable组件,通过:data="goods"传入商品数据。 - 自定义
MyTable的表头(#head插槽):定义 "编号、名称、图片、标签" 四列。 - 自定义
MyTable的行内容(#body插槽):- 接收
MyTable传递的item(当前商品)和index(索引)。 - 依次渲染:编号(
index + 1)、商品名称(item.name)、商品图片(img :src="item.picture")、可编辑标签(MyTag v-model="item.tag")。
- 接收
- 使用
-
脚本部分:
- 导入并注册
MyTable和MyTag组件。 data()中定义goods数组:存储商品数据(包含id、picture、name、tag等字段),作为表格的数据源。
- 导入并注册
-
样式部分:
- 定义页面容器样式(宽度、居中)和图片的补充样式。
二、运行流程
初始化渲染:
TableCase组件挂载,渲染MyTable组件,并将goods数组传给MyTable的dataprops。MyTable渲染表头:使用#head插槽的内容(四个th)。MyTable循环goods数组,为每个商品渲染一行(tr),并通过#body插槽渲染行内容:- 编号、名称、图片按商品数据直接展示。
- 标签列渲染
MyTag组件,通过v-model="item.tag"绑定当前商品的tag值,初始显示item.tag的文本。
标签编辑交互:
- 双击
MyTag的文本区(div):触发handleClick,isEdit变为true,切换到输入框(input)显示。 - 输入框自动聚焦(需
v-focus指令或$nextTick逻辑支持),用户修改内容后:- 按回车:触发
handleEnter,验证非空后,通过$emit('input', 新值)通知App,item.tag被更新为新值,随后isEdit变为false,回到文本显示模式。 - 失焦(点击其他区域):触发
@blur,isEdit变为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.inp为undefined)而报错,或者无法成功聚焦。
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 事件(如keyup、click等)触发时,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.value比this.$refs.inp.value更直接、更稳健,减少了对ref的依赖,符合事件驱动的编程逻辑。两种方式在当前场景下效果一致,但
e.target.value是更推荐的写法。