Vue框架


Vue3组件基础:ref引用、动态组件、插槽、自定义指令


前面简单介绍了组件的相关,比如监听,组件关系和组件的数据共享,还有自定义事件,这样一个界面就可以为不同的组件相互作用结合形成,数据传递则依靠指令和之间【后代可以使用provide和inject便捷传输】 复杂的数据传递一般使用vuex

其实很多得概念之前就提到了,比如这里得ref引用;【这个在Spring的IOC的属性注入的位置,对象属性就是ref,普通的属性就是value】,之前的响应式数据传递,就配置全局数据项,就可以不写.value

ref引用

ref引用就是对象的引用,ref是帮助programmer在不依赖JQuery的情况下,获取DOM元素或者组件的引用【之前Jq最方便的就是简化了对象的操作,特别是$(选择器:过滤器)】

Vue里面是不建议使用Jq的 ,因为容易造成引用的混乱,所以就要使用ref来操作DOM或者组件。每一个vue的组件实例上,都包含一个$refs对象,里面存储着对应的DOM元素或者对于组件的引用,默认情况下,其默认指向一个空对象

之前分享组件的基础的时候,就提过this代表的就是当前组件的实例,这里可以看一下其内容

[[Target]]: Object
num: (...)
username: (...)
$: (...)
$attrs: (...)
$data: (...)
$el: (...)
$emit: (...)
$forceUpdate: (...)
$nextTick: (...)
$options: (...)
$parent: (...)
$props: (...)
$refs: Object
$root: (...)
$slots: (...)
$watch: (...)
_: Object

可以看到有很多的内置的对象,这里就有一个$refs

使用ref引用DOM元素

如果想要对页面上的某个DOM元素进行操作,就给需要操作的DOM结点添加ref属性

  • 为对应的DOM添加ref属性,指明引用的名称
<h1 ref="myh1">LifeCircle子组件</h1>
  • 通过this.$refs.引用名称,获取到dom元素的引用

  • 通过该引用操作DOM元素

<button type="button" @click="getDom">获取DOM结点h1</button>

methods:{
			getDom() {
				console.log(this.$refs.myh1) //因为会将带有ref属性的结点注册到$refs中,所以这里想要使用myh1,就直接.引用即可
			}
		}

这里打印的结果就是: < h1>LifeCircle子组件< /h1>;成功获取到了该组件对应页面上的DOM结点,比如这里想要修改文本的颜色: this.$refs.myh1.style.color = 'red’就可以设置红色

使用ref引用组件

如果想要引用页面上的组件【组件对于父组件来说也是一个标签,和上面的DOM一样是一个结点】

  • 为要引用的组件加上ref引用名称
  • 使用this.$refs.引用名称就可以获取到组件的实例对象,通过.methods就可以直接使用组件的methods结点中定义的方法

这里比如在consum-kid组件中定义了方法setColor,可以获取组件页面中的dom元素修改其背景颜色,在App中点击按钮达到效果

//consum-kid.vue
X * 2 + 1的结果为 :<span ref='myspan' :style="{'background-color':color}">{{count * 2 + 1}}</span>

methods:{
			setColor() { //在子组件中定义了一个set方法,在另一个组件中点击按钮就可以执行该方法,那么就要使用组件的引用
				this.$refs.myspan.style.backgroundColor = 'pink'
			}
		}

//父组件App.vue
<button type="button" @click="changeColor">改变子组件的字体的颜色</button>

<cosum-kid ref="conKid"></cosum-kid>

methods:{
	  changeColor(){
		  this.$refs.conKid.setColor()        //获取子组件调用其set方法修改颜色
	  }
  }

这样通过ref引用就可以方便操作组件及上面的DOM结点

实例----控制文本框和按钮的按需转换

通过布尔值inputVisible来控制组件中的文本框于按钮的按需切换

当为true的时候就可以展示文本输入框,要想展示的一瞬间获得焦点,就使用元素js的方法.focus()即可;所以通过ref引用获取文本框即可

<hr color="#BA8B00">
	<input type="text" ref="ipt" v-if="inputVisible"  />
	<button v-else type="button" @click="showInput" class="btn btn-primary">展示input的输入框</button>
<hr color="#BA8B00" />

methods:{
			showInput() {
				this.inputVisible = true
				this.$refs.ipt.focus()
			}
		}

这样点击按钮就可显示文本框,但是控制台报错:Uncaught TypeError: Cannot read properties of undefined (reading ‘focus’) — 也就是浏览器并没有检测到文本框undefined

DOM元素的更新是异步的,也就是异步的任务【因为要重新渲染结点,属于宏任务】,在EventLoop中,这是在一个消息队列中,主任务就是优先执行,所以会立即去执行this.$refs,这个时候并没有获取到结点,报错

this.$nextTick(cb)将cb延迟到DOM更新完

上面就发现因为组件的更新是异步的,而js是将栈中的所有的主任务执行完毕才会执行异步任务,所以调用不应该在主任务,vue提供了内部函数$nextTick(cb),会将回调函数推迟到下一个DOM更新周期之后执行,也就是登组件重新渲染更新完毕之后,才会执行cb函数,保证cb操作的最新的回调函数

methods:{
	showInput() {
		this.inputVisible = true
		//把对dom元素的操作推迟到页面的DOM更新完毕之后执行
		this.$nextTick(() => {
			this.$refs.ipt.focus()
		})
	}
}

这里将执行语句放到匿名回调函数中,等待更新完毕之后执行,相当于也是异步的任务

动态组件

动态组件指的是动态切换组件的显示和隐藏,vue提供了一个内置的< compnent>组件,用来实现组件的动态渲染 【之前的普通的DOM结点的显示可以使用v-if或者v-show】

  • component是组件的占位符
  • 通过js 属性动态指定要渲染的组件的名称
  • < component is=“要渲染的组件的名称”>< /component>

也就是使用component标签占据位置,通过这个标签的is属性就可以动态绑定组件

<template>
  <img alt="Vue logo" src="./assets/logo.png" /><br/>
  <button type="button" @click="changeColor">改变子组件的字体的颜色</button><br>
  <button type="button" @click="showComp">动态显示组件</button><br>
  姓名<input type="text" v-model.lazy="info.username"/>
  <hr/>
<!--  <life-circle v-if="flag"></life-circle>
  <cosum-kid ref="conKid"></cosum-kid> -->
  <component :is="conName"></component>
</template>


 data() {
	  return {
		  conName:'',

showComp() {
		  this.conName = 'LifeCircle'
	  }

这样只要点击按钮,就可以显示LifeCircle组件

keep-alive保持组件状态

这里的component占位的效果,其实也是动态创建或者销毁组件实例,当切换其他的组件的时候,之前的组件实例就被销毁,第二次切换回来的时候就是重新创建组件实例,所以之前的状态就会恢复到最初的状态, 如果想要不销毁,那么就要使用keep-alive

默认情况下,切换动态组件时 无法保持组件的状态,这个时候可以使用vue内置的< keep- alive>组件包裹component标签保持动态组件的状态

<keep-alive>
	<component :is = 'conName'></component>
</keep-alive>

这样组件实例切换的时候就不会被销毁,组件实例就会被缓存到浏览器的内存中,一般都会使用keep-alive来保持之前页面的状态

插槽slot

插槽是vue为组件的封装er提供的能力,允许programmer在封装组件的时候,把不确定的、希望由用户指定的部分定义为插槽

这样就可以通过插槽将html小的页面插入到封装好的组件中,插槽就是在组件的封装期间,为用户预留的占位符;上面的动态组件的component也是占位符,但是占的是整个组件的位,这里占位站的就只是DOM的位置

slot的基本使用

在封装组件期间,可以通过< slot>元素定义插槽,从而为用户预留的内容进行占位

<template>
	<p>
        这是子组件的第一个p标签
    </p>
	<slot></slot>   <!-- 插槽:不确定放什么内容,后面组件的使用者进行自定义 -->
	<p>
        这是子组件的第二个P标签
    </p>
</template>

使用者,也就是父组件调用的时候,使用双目的子组件的标签,标签之间内容都会被填充到插槽

这里可以演示一下

<template>
	<p>SlotTest组件  ---- 这是第一个p标签;下面就是使用者自定义的内容</p>
	<slot></slot>
	<p>这是第二个p标签</p>
</template>

<script>
	export default {
		name: 'SlotTest',
	}
</script>

<style lang="less" scoped>
</style>



-------------------父组件App.vue进行调用--------------
  <!-- 调用子组件时,标签之间的内容就会被填充到子组件的插槽中 -->
  <slot-test>
	  <div>
		  我是App根组件自定义的内容
	  </div>
  </slot-test>
没有预留插槽,自定义内容丢弃

就是如果在组件中没有定义slot插槽,那么就算在父组件使用时,在标签间定义了内容,也不会填充,因为没有插槽,那么效果就是会被自动舍弃

后备内容【默认内容】— slot标签域内容

封装组件时,可以为预留的slot 插槽提供后备内容【默认内容】,如果使用者没有为插槽提供内容,那么就会使用后备内容,提供了就会覆盖

<template>
	<p>
        第一个p标签
    </p>
	<slot>
    	插槽的后备内容都在这里 【 如果使用者不提供,改内容生效】 --- 这样也实现动态页面
    </slot>
	<p>
        第二个p标签
    </p>
</template>

具名插槽

上面的只是定义了一个插槽,插槽标签slot中的内容就是后备内容;如果需要在封装组件的时候预留多个插槽结点,则需要为每一个插槽指定具体的name名称,这种含有名称的插槽为具名插槽

<template>
	<header>
		<!-- 页面的标题 -->
		<slot name="header">
			后备内容
		</slot>
	</header>
	<main>
		<slot>
			这是一个默认的插槽
		</slot>
	</main>
	<footer>
		<slot name="footer">
			后备内容 --- 具名插槽
		</slot>
	</footer>
</template>

<script>
	export default {
		name: 'SlotTest',
	}
</script>

<style lang="less" scoped>
</style>

如果没有指定name名称的插槽,那么这个插槽的名称为default — 所以只能由一个default

具名插槽插入内容 v-slot:名称 【slot已经弃用】

比如这里定义了3个插槽

<template>
	<div>
		<header>
			<!-- 页面的标题 -->
			<slot name="header">
				后备内容 ---- header
			</slot>
		</header>
		<slot>
			默认的插槽
		</slot>
		<footer>
			<slot name="footer">
				后备内容 --- 具名插槽
			</slot>
		</footer>
	</div>
</template>

在App根组件中要使用这个组件,传入了需要插入的自定义的内容

<slot-test>
	  <div>
		  <h3>春晓</h3>
		  <center>孟浩然</center>
		  <p>春眠不觉晓,处处闻啼鸟</p>
		  <p>夜来风雨声,花落知多少</p>
	  </div>
  </slot-test>

最终渲染的结果就是: 所有的内容都进入了默认插槽;也就是说,在不特殊说明的情况下,插入的内容都会进入默认插槽

要让内容进入具名插槽,需要使用template标签包裹内容,并且使用v-slot指定剧名插槽的名称

 <!-- 调用子组件时,标签之间的内容就会被填充到子组件的插槽中 -->
  <slot-test>
		  <template v-slot:header>
			  <h3>春晓</h3>
		  </template>
		  <template v-slot:default>
			  <center>孟浩然</center>
		  </template>
		  <template v-slot:footer>
			  <p>春眠不觉晓,处处闻啼鸟</p>
			  <p>夜来风雨声,花落知多少</p>
		  </template>
  </slot-test>

对要插入的内容使用template标签包裹,并且使用v-slot指明具名插槽的名称,使用: ,不是=号,并且直接写名称,不用加引号,不然会报错

Error: Codegen node is missing for element/if/for node. Apply appropriate transforms first.

这里的问题就是因为:template标签放的位置有问题,template必须是最外层的标签,外面不能包裹div了,报错就是因为组件标签里的内容包裹在div中:happy:,template应该是最外面 ----- div也是属于DOM结点,template才能将其渲染到界面上 ---- 所以template必须包裹所有的DOM

  <slot-test>
	  <template v-slot:header>
	  		<h3>春晓</h3>
	  </template>
	  <span style="align-content: center;">作者:孟浩然</span>
	  <template v-slot:footer>
		  <p>春眠不觉晓,处处闻啼鸟</p>
		  <p>夜来风雨声,花落知多少</p>
	  </template>
  </slot-test>
</template>

默认插槽可以省略template,具名插槽不能省略

具名插槽简写 : #插槽名

和v-on和v-bind一样,v-slot也可以简写,把参数之前的内容v-slot:替换为字符#, 比如

<template v-slot:footer>

<template #footer>

这里可以修改上面的代码

<slot-test>
	  <template #header>
	  		<h3>春晓</h3>
	  </template>
	  <template #default>
		  <span style="align-content: center;">作者:孟浩然</span>
	  </template>
	  <template #footer>
		  <p>春眠不觉晓,处处闻啼鸟</p>
		  <p>夜来风雨声,花落知多少</p>
	  </template>

作用域插槽

之前的作用域插槽slot-scope已经弃用,在封装组件的过程中,可以为预留的slot插槽绑定props数据,带有props数据的slot就是作用域插槽

也就是说插槽中有数据,就是为slot动态绑定数据

<slot :prop = 'XXX'></slot>

使用者使用的时候传入数据的时候,还可以指定prop绑定的数据

<template #default='scope'>
	xxx
</template>

在插槽的名称后面通过=‘scope’ ;然后这个scope就可以接收到上面的属性prop, 也就是实现了子组件的数据传输到了父组件

<slot name="header" :info = 'infomation' :msg = 'message'>
    
data() {
			return {
				infomation:{
					stuname: 'Cfeng',
					stuClass: 'HC2001',
					stuMajor: 'CS'
				},
				message: 'Hello,Cfeng!!'
			}
		}
    
--------------------------------------------------------------
  <slot-test>
	  <template #header='scope'>
	  		<p>{{scope}}</p>
	  </template>
  </slot-test>

然后打印的结果就是: { “info”: { “stuname”: “Cfeng”, “stuClass”: “HC2001”, “stuMajor”: “CS” }, “msg”: “Hello,Cfeng!!” }】

也就是scope会接收插槽传递的所有的属性形成的JSON对象

解构作用域插槽

之前已经使用过很多次解构了,就是可以直接加上{},然后就可以直接取出对象中的属性;

<template #header='{msg,info}'>

这样就可以直接使用msg的值,而不再需要使用scope.msg来获得数据

作用域插槽应用

作用域插槽的最主要的特点就是组件的多态,就类似之前的java的abstract一样;不会具体定义,交给用户来决定数据的样式

比如这里子组件提供了一个table

<template>
	<div>
		<table style="border: 1px solid rosybrown;">
			<th>
				<td>序号</td>
				<td>姓名</td>
				<td>状态</td>
			</th>
			<tr v-for="item in list":key="item.id">
				<td>{{item.id}}</td>
				<td>{{item.name}}</td>
				<td>{{item.state}}</td>
			</tr>
		</table>
	</div>
</template>

<script>
	export default {
		name: 'SlotTest',
		data() {
			return {
				list:[
					{id:1,name:'张三',state:true},
					{id:2,name:'李四',state:true},
					{id:3,name:'Cfeng',state:true}
				]
			}
		}
	}
</script>

这里的子组件将数据直接给到了template中,但是问题就是这里的state,并不知道用户想要一个什么样子的样式,是复选框,还是什么;所以这里不要直接将数据按照特定的格式渲染,而是交给使用者App来按需操作

<template>
	<div>
		<table style="border: 1px solid rosybrown;">
			<th>
				<td>序号</td>
				<td>姓名</td>
				<td>状态</td>
			</th>
			<tr v-for="item in list":key="item.id">
				<slot :item = 'item'>
					<!-- 插槽中放一个默认的渲染的样式,用户如果忘记可以后备 -->
					<td>{{item.id}}</td>
					<td>{{item.name}}</td>
					<td>{{item.state}}</td>
				</slot>
			</tr>
		</table>
	</div>
</template>

在App.vue中通过作用域插槽的操作,来进行具体的渲染

  <slot-test>
	  <template #default='{item}'>
		  <td>{{item.id + 1}}</td>
		  <td>{{item.name}}</td>
		  <td>
			  <input type="checkbox" :checked="item.state" />
		  </td>
	  </template>
  </slot-test>

父组件就可以解构出item属性然后使用,自定义样式;比如这里将state渲染成了一个复选框的样式

⚠: Whitespace was expected. 这是因为v-for指令和:key两个属性之间需要空格

<tr v-for = 'item in list' :key='item.id'>  //因为这是属性绑定,key不是修饰符,绑定的key属性,两个不同的指令或者属性之间要有空格

自定义指令

vue官方提供了v-for,v-bind等内置指令,除此之外vue还允许开发者自定义指令,vue自定义指令主要有两类:

  • 私有自定义指令
  • 全局自定义指令

私有自定义指令 directives结点下声明

这里先定义一个组件MyHome,其中有一个文本框,现在的需求: 当渲染出组件的时候,文本框自动获得焦点

  1. 可以使用之前的ref来动态获得组件【这里使用的是动态显示组件,点击按钮会显示MyHome组件,然后这里的渲染必须是异步任务,所以需要使用$nextTick,这里前面是获取组件,后面是获取组件上的结点
<input ref="ipt" type="text" class="form-control" />

<keep-alive>
	<component :is="conName" ref="myHome"></component>
 </keep-alive>

showComp() {
		  this.conName = 'MyHome'
		  this.$nextTick(() => {//变成异步任务
			  this.$refs.myHome.$refs.ipt.focus()
		  })
	  }

这里直接给动态组件加上了ref属性,也是可以获取到这里具体的组件的

这里可以使用自定义指令,比如为input绑定自定义指令v-focus

<input type="text" class="form-control" v-focus/>

[Vue warn]: Failed to resolve directive: focus --自定义指令必须要进行声明

使用自定义指令,必须以v-开头,但是在directives下面声明私有指令时,不用v- 这里的directives结点和data等结点平级

directives: {
	//自定义一个私有指令
	focus: {
		//当绑定元素插入到DOM时,自动会触发mounted函数
		mounted(e1) {
			e1.focus()
		}
	}
}

这里mounted的参数就是自定义指令绑定的DOM结点;mounted函数的执行时机: 指令绑定的结点渲染到DOM结构的时候自动触发

<input type="text" class="form-control" v-focus />

directives:{
			focus: {
				mounted(e) {
					e.focus()
				}
			}
		}

自动执行mounted方法,获取焦点; 所以自定义的私有指令就是在对应的组件的directives结点中声明指令,然后就可以调用了,mounted函数可以获取到绑定的元素DOM

全局自定义指令

上面的v-focus指令声明在MyHome组件中,在其他的组件中是不能使用这个指令的;就类似之前的组件的全局注册一样;只要在main.js进行指令声明就可以使用,使用spa_app.directive方法可以声明,第一个是名称,第二个是指令的方法体

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'

//导入axios
import axios from 'axios'

const spa_app = createApp(App)

//在mount之前进行配置
//声明请求的相对路径
axios.defaults.baseURL = 'https://www.escook.cn/api'

//全局注册挂载
spa_app.config.globalProperties.$ajax = axios

spa_app.config.unwrapInjectedRef = true
spa_app.directive('focus',{
	mounted(e) {
		e.focus()
	}
})

spa_app.mount('#app')

还是要在spa_appmout之前进行声明,和组件的全局注册类似

全局声明自定义指令之后,不管在哪个组件中都可以使用该指令,这里将私有的v-focus声明去除;在MyHome组件中还是可以使用

updated函数保持v-focus持续触发

上面的v-focus指令有点缺陷: — mounted函数只是在元素第一次插入DOM时被调用,当DOM更新时mounted函数不会被触发; 与之相比,updated函数【之前分享组件的生命周期的时候,组件的几个函数和DOM的函数相同,mounted就是渲染到页面后触发,而updated就是每次页面更新都会触发】

spa_app.directive('focus',{
	mounted(e) {
		e.focus()
	},
	updated(e) {
		e.focus()
	}
})

这样声明之后就可以渲染和更新的时候都会获取焦点【和组件的生命周期函数是相同的】组件也可以看作父组件的一个DOM结点

<template>
	<div class="home-container">
		<p>MyHome组件 ---- {{count}}</p>
		<hr color="yellowgreen"/>
		<input ref="ipt" type="text" class="form-control" v-focus /><br>
		<button type="button" class="btn btn-primary" @click="count++">数值+ 1</button>
	</div>
</template>

这里点击按钮页面更新,这个时候会执行updated函数,还是会获取焦点

在vue2项目中使用自定义指令,是没有ed的,就是mount和update【提一句】

函数简写

如果mounted函数和updated函数的逻辑完全相同,就可以将方法的第二项完全变成一个方法,而不是方法体

spa_app.directive('focus',(e) => {
	e.focus()
})

指令的参数值

自定义指令声明的方法的第一个形参指代的DOM对象,第二个形参指定的时指令绑定的参数对象,通过.value获取参数对象的具体值

vue提供的指令都是支持参数的,自定义指令也是支持的,在绑定指令的时候,可以通过等号的形式为指令绑定具体的参数值

<input type='text' v-model.number = 'count' v-focus v-color= "'red'" />

比如上面就为v-color自定义指令绑定了参数red;

声明指令的时候,可以通过函数的形参binding来接收绑定的参数,第一个参数是指代的DOM对象,第二个参数就是binding参数; 注意binding接收的是一个对象,获取red要使用binding.value

spa_app.directive('color', (e,binding) => {
	e.style.backgroundColor = binding.value
})

子组件使用v-color指令时,需要注意里面还有单引号; 因为如果时属性绑定,后面的变量就要包裹在引号中,现在这个变量时字符串,所以需要有两重的引号

Table案例

案例达到的效果就是商品列表,主要步骤

首先建立一个vite项目table-demo,初始化项目

npm init vite-app table-demo

cd table-demo

npm i

npm i less -D

npm i axios -S

npm run dev

同时要导入bootstap.css,修改index.css全局的样式

:root {
	font-size: 12px;
}

body {
	padding: 8px;
}
  • 请求商品列表的数据 ---- 上面安装了axios包,接下来就是在mian.js中进行配置
 methods:{
	async getGoodsList() {
		const {data: res} = await this.$ajax.get('/goods')
		if(res.status !== 0) return console.log("请求商品列表失败")
		this.goodsList = res.data
	}  
  },
  components: {
    
  },
  created() {
  	this.getGoodsList()
  }
}

这里需要注意的就是定义的全局属性$ajax要通过this进行调用,不能直接写出,不然undefined

  • 封装MyTable组件

封装的要求: 用户通过名为data的prop属性,为MyTable组件指定数据源; 在Mytable组件中,预留名称为header的具名插槽;同时要预留名为body的作用域插槽

这里模板结构还是基于的bootstrap进行渲染,这里的作用域插槽携带的数据就是row当前的数据对象和当前的索引index

<template>
  <!-- <button type="button" class="btn btn-primary">jkj</button> -->
  <my-table :data = 'goodsList'>
	  <template #header>
		  <tr>
		  	<th>#</th>
		  	<th>商品名称</th>
		  	<th>价格</th>
		  	<th>标签</th>
		  	<th>操作</th>
		  </tr>
	  </template>
	  <template #body="{row,index}">
		  <td>{{index + 1}}</td>
		  <td>{{row.goods_name}}</td>
		  <td>¥{{row.goods_price}}</td>
		  <td>{{row.tags}}</td>
		  <td>
			  <button type="button" class="btn btn-danger btn-sm">删除</button>
		  </td>
	  </template>
  </my-table>
</template>

<script>
import MyTable from './components/my-table/MyTable.vue'

export default {
  name: 'App',
  data() {
  	return {
		goodsList:[],
	}
  },
  methods:{
	async getGoodsList() {
		const {data: res} = await this.$ajax.get('/goods')
		if(res.status !== 0) return console.log("请求商品列表失败")
		this.goodsList = res.data
	}  
  },
  components: {
    MyTable,
  },
  created() {
  	this.getGoodsList()
  }
}
</script>

<style lang="less" scoped></style>
  • 实现删除的功能

为按钮添加事件处理函数,根据id删除goodsList中的数据

 <button type="button" class="btn btn-danger btn-sm" @click="onRemoveGoods(row.id)">删除</button>

onRemoveGoods(id) {
		this.goodsList = this.goodsList.filter(x => x.id !== id) //过滤出不是id的所有的数据
	}
  • 实现添加标签的功能
<td>
			  <!-- 循环渲染标签 -->
			  <span class="badge badge-warning ml-2" v-for="tag in row.tags">{{tag}}</span>
		  </td>

标签tags也是一个数组,所以需要使用class进行渲染

文本输入框的焦点事件blur,就是失去焦点的时候就会触发

这里的所有的代码都是在App.vue完成的

<template>
  <!-- <button type="button" class="btn btn-primary">jkj</button> -->
  <my-table :data = 'goodsList'>
	  <template #header>
		  <tr>
		  	<th>#</th>
		  	<th>商品名称</th>
		  	<th>价格</th>
		  	<th>标签</th>
		  	<th>操作</th>
		  </tr>
	  </template>
	  <template #body="{row,index}">
		  <td>{{index + 1}}</td>
		  <td>{{row.goods_name}}</td>
		  <td>¥{{row.goods_price}}</td>
		  <td>
			  <!-- 基于当前的inputVisible,来控制input和button的按需显示 -->
			  <input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim = 'row.inputValue' @blur="onInputConfirm(row)" @keyup.enter="onInputConfirm(row)" @keyup.esc = "row.inputValue =''"/>
			  <button type="button" class="btn btn-primary btn-sm" v-else @click="row.inputVisible = true">+Tag</button>
			  <!-- 循环渲染标签 -->
			  <span class="badge badge-warning ml-2" v-for="tag in row.tags">{{tag}}</span>
		  </td>
		  <td>
			  <button type="button" class="btn btn-danger btn-sm" @click="onRemoveGoods(row.id)">删除</button>
		  </td>
	  </template>
  </my-table>
</template>

<script>
import MyTable from './components/my-table/MyTable.vue'

export default {
  name: 'App',
  data() {
  	return {
		goodsList:[],
	}
  },
  methods:{
	async getGoodsList() {
		const {data: res} = await this.$ajax.get('/goods')
		if(res.status !== 0) return console.log("请求商品列表失败")
		this.goodsList = res.data
	},
	onRemoveGoods(id) {
		this.goodsList = this.goodsList.filter(x => x.id !== id) //过滤出不是id的所有的数据
	},
	onInputConfirm(goods) {
		//因为双向绑定,所以输入的值在inputValue中
		const val = goods.inputValue
		goods.inputValue = ''
		//隐藏文本框
		goods.inputVisible = false
		//判断重复和空
		if(!val || goods.tags.indexOf(val) !== -1) return
		//将内容存放到tags中
		goods.tags.push(val)
	}
  },
  components: {
    MyTable,
  },
  created() {
  	this.getGoodsList()
  }
}
</script>

<style lang="less" scoped>
	.ipt-tag {
		width: 80px ;
		display: inline;
	}
</style>

这样就完成了列表的标签的添加

在这里插入图片描述

vue中关于组件部分就介绍到这里,接下来就是路由了🎉

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐