【组件封装】uniapp vue3 封装一个完整的Tabs(标签页)组件教程,功能由简到杂实现讲解。

news2025/1/10 17:57:30

文章目录

  • 前言
  • 一、简单版Tabs
    • 代码实现:
  • 二、下划线带动画的Tabs
    • API回顾:
    • 代码实现:
  • 三、内容区域滑动切换+切换动画
    • 代码实现:
    • (2)禁用手势滑动切换
    • (3)内容区域换为插槽
  • 四、标签栏可滚动
    • 代码实现


前言

手把手教你封装一个移动端 Tabs组件(标签页),功能由简到杂以uniapp vue3为代码示例。


一、简单版Tabs

实现一个最简单版的Tabs,下划线无动画无手势切换等,如下图所示:

请添加图片描述

实现说明:标签通过flex布局排列,下划线通过伪类绝对定位在选中项底部,选中项通过索引记录动态添加激活class

代码实现:

tabs.vue

<template>
	<view class="tabs">
		<view :class="['tab',{active:index==selectIndex}]" v-for="(item,index) in  list" :key="index"
			@click="handleSelect(index)">{{item}}</view>
	</view>
</template>

<script setup>
	import {
		computed,
		ref
	} from 'vue'
	const props = defineProps({
		//标签配置
		list: {
			type: Array,
			default: () => []
		},
		//激活颜色
		activeColor: {
			type: String,
			defalut: 'deepskyblue'
		}
	})

	//激活颜色
	const activeColor = computed(() => {
		return props.activeColor || 'deepskyblue'
	})

	//当前选中索引
	const selectIndex = ref(0)

	//切换标签
	const handleSelect = (index) => {
		if (index !== selectIndex.value) {
			selectIndex.value = index
		}
	}
</script>

<style lang="scss" scoped>
	.tabs {
		width: 100%;
		display: flex;
		align-items: center;
		background-color: #fff;

		.tab {
			flex: 1;
			padding: 25rpx 10rpx;
			width: 0;
			box-sizing: border-box;
			text-align: center;
			white-space: nowrap;
			overflow: hidden;
			text-overflow: ellipsis;
			color: #333;
			font-size: 30rpx;
			position: relative;

			&.active {
				font-weight: bold;
				color: v-bind(activeColor);
                //下划线
				&::after {
					display: block;
					content: '';
					position: absolute;
					bottom: 0;
					left: 50%;
					transform: translateX(-50%);
					width: 30px;
					height: 6rpx;
					background-color: v-bind(activeColor);
					border-radius: 6rpx;
				}
			}
		}
	}
</style>

ps:注意scss中使用了v-bind引用vue变量activeColor动态设置标签和下划线激活颜色


二、下划线带动画的Tabs

接下来功能升级,要求切换标签的时候下划线有滑动动画,如下图所示

请添加图片描述

实现说明:因为下划线要有滑动动画,就不能相对于选中项绝对定位,而应该基于一个固定的父级元素定位,这个父元素就是组件最外层容器。选中后通过计算下划线到父容器距离(也就是下划线到页面最左边距离)确定绝对定位的left值,同时设置过渡动画。

API回顾:

在uniapp中由于底层使用引擎不同小程序或者app中无法像h5一样进行任何dom操作,只能通过官方提供的api来获取节点信息。

uni.createSelectorQuery()可用于获取节点信息,并结合如下使用方式:

import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance();

const query = uni.createSelectorQuery().in(instance.proxy);
query
  .select("#id")
  .boundingClientRect((data) => {
    console.log("得到布局位置信息" + JSON.stringify(data));
    console.log("节点离页面顶部的距离为" + data.top);
     console.log("节点离页面左边的距离为" + data.left);
  })
  .exec();

来获取节点宽高和距离窗口左边或者顶部距离。

下划线位置计算:
在这里插入图片描述
下划线绝对定位left值=a段长度,a=b+标签宽/2,而b为标签与页面左边距离,b和标签宽都可以通过节点信息api获取
ps:因为下划线设置了css属性值 transform: translateX(-50%),向左平移自身一半,所以left值为a

代码实现:

tabs.vue

<template>
	<view class="tabs">
	    <!-- 标签栏 -->
		<view :class="['tab',{active:index==selectIndex}]" v-for="(item,index) in  list" :key="index"
			@click="handleSelect(index)">{{item}}</view>
		<!-- 下划线 -->
		<view :class="['underline',{transition:left!==null}]"></view>
	</view>
</template>

<script setup>
	import {
		computed,
		ref,
		onMounted,
		nextTick,
		getCurrentInstance
	} from 'vue'
	const props = defineProps({
		//标签配置
		list: {
			type: Array,
			default: () => []
		},
		//激活颜色
		activeColor: {
			type: String,
			defalut: 'deepskyblue'
		}
	})

	//激活颜色
	const activeColor = computed(() => {
		return props.activeColor || 'deepskyblue'
	})
	
	//当前选中索引
	const selectIndex = ref(0)
	
	//切换标签
	const handleSelect = (index) => {
		if (index !== selectIndex.value) {
			selectIndex.value = index
			setPosition()
		}
	}
	
	//下划线离父元素左边距
	const left = ref(null)
	//组件实例
	const instance=getCurrentInstance()
	//设置下划线位置
	const setPosition = () => {
		nextTick(() => {
			let query = uni.createSelectorQuery().in(instance.proxy)
			query.select(".active").boundingClientRect(data => {
				//定位距离=选中标签项与左距离+标签宽一半
				left.value =`${data.left+data.width/2}px`

			}).exec()
		})
	}
	
	onMounted(() => {
		//设置下划线初始位置
	   setPosition()
	})
</script>

<style lang="scss" scoped>
	.tabs {
		width: 100%;
		display: flex;
		align-items: center;
		background-color: #fff;
		position: relative;
	
		.tab {
			flex: 1;
			padding: 25rpx 10rpx;
			width: 0;
			box-sizing: border-box;
			text-align: center;
			white-space: nowrap;
			overflow: hidden;
			text-overflow: ellipsis;
			color: #333;
			font-size: 30rpx;
			position: relative;
			&.active {
				font-weight: bold;
				color: v-bind(activeColor);
			}
		}
	}
	//下划线
	.underline {
		position: absolute;
		width: 30px;
		height: 6rpx;
		background-color: v-bind(activeColor);
		border-radius: 6rpx;
		bottom: 0;
		left: v-bind(left);
		transform: translateX(-50%);
		display: none;
		&.transition {
			display: block;
			transition: all 0.3s;
		}
	}
</style>

ps:下划线绝对定位left值在scss通过 v-bind动态访问vue变量


三、内容区域滑动切换+切换动画

在上述示例基础上继续扩展功能,目标是切换标签页支持内容区域带动画同时内容区域滑动可以切换标签,如下图所示:

请添加图片描述

实现说明:结合swiper轮播图组件封装,内容区域使用swiper作为父容器,因为swiper支持手势滑动和滑动动画。

代码实现:

tabs.vue

<template>
	<view class="comp-container">
		<view class="tabs">
			<!-- 标签栏 -->
			<view :class="['tab',{active:index==selectIndex}]" v-for="(item,index) in  list" :key="index"
				@click="handleSelect(index)">{{item}}</view>
			<!-- 下划线 -->
			<view :class="['underline',{transition:left!==null}]"></view>
		</view>
		<!-- 内容区域 -->
		<view class="content">
			<swiper class="swiper" :current="selectIndex" @change="onSwiperChange">
				<swiper-item class="swiper-item" v-for="item in list" :key="item">
					<scroll-view scroll-y style="height:100%">
						<!-- 页面内容 -->
						<view class="main">{{item}}</view>
					</scroll-view>
				</swiper-item>
			</swiper>
		</view>
	</view>
</template>

<script setup>
	import {
		computed,
		ref,
		onMounted,
		nextTick,
		getCurrentInstance
	} from 'vue'
	const props = defineProps({
		//标签配置
		list: {
			type: Array,
			default: () => []
		},
		//激活颜色
		activeColor: {
			type: String,
			defalut: 'deepskyblue'
		}
	})

	//激活颜色
	const activeColor = computed(() => {
		return props.activeColor || 'deepskyblue'
	})

	//当前选中索引
	const selectIndex = ref(0)

	//切换标签
	const handleSelect = (index) => {
		if (index !== selectIndex.value) {
			selectIndex.value = index
			setPosition()
		}
	}

	//下划线离父元素左边距
	const left = ref(null)
	//组件实例
	const instance = getCurrentInstance()
	//设置下划线位置
	const setPosition = () => {
		nextTick(() => {
			let query = uni.createSelectorQuery().in(instance.proxy)
			query.select(".active").boundingClientRect(data => {
				//定位距离=选中标签项与左距离+标签宽一半
				left.value = `${data.left+data.width/2}px`

			}).exec()
		})
	}

	onMounted(() => {
		//设置下划线初始位置
		setPosition()
	})
	
	
	//手势切换回调
	const onSwiperChange = e => {
		if (e.detail.current !== selectIndex.value) {
			handleSelect(e.detail.current)
		}
	}
</script>

<style lang="scss" scoped>
	.comp-container {
		height: 100%;
		display: flex;
		flex-direction: column;
		background-color: #f2f2f2;
		overflow: hidden;
	}
	.tabs {
		width: 100%;
		display: flex;
		align-items: center;
		background-color: #fff;
		position: relative;
		flex-shrink: 0;

		.tab {
			flex: 1;
			padding: 25rpx 10rpx;
			width: 0;
			box-sizing: border-box;
			text-align: center;
			white-space: nowrap;
			overflow: hidden;
			text-overflow: ellipsis;
			color: #333;
			font-size: 30rpx;
			position: relative;

			&.active {
				font-weight: bold;
				color: v-bind(activeColor);
			}
		}
	}

	//下划线
	.underline {
		position: absolute;
		width: 30px;
		height: 6rpx;
		background-color: v-bind(activeColor);
		border-radius: 6rpx;
		bottom: 0;
		left: v-bind(left);
		transform: translateX(-50%);
		display: none;

		&.transition {
			display: block;
			transition: all 0.3s;
		}
	}

	.content {
		flex: 1;
		height: 0;
		overflow: hidden;

		.swiper {
			height: 100%;

			.swiper-item {
				height: 100%;
			}
		}

		.main {
			background: #f2f2f2;
			text-align: center;
			padding: 30rpx;
			box-sizing: border-box;
		}
	}
</style>

页面引用:
index.vue

<template>
	<view class="container">
		<Tabs :list="list" />
	</view>
</template>

<script setup>
	import Tabs from '@/components/tabs.vue'
	import {
		ref
	} from 'vue'
	const list = ref(['手机', '电脑', '电视机', '洗衣机'])
</script>

<style lang="scss" scoped>
	.container {
		height: 100vh;
		background-color: #f2f2f2;
	}
</style>

说明:为了使内容区域可滚动,内嵌了scroll-view,而scroll-view需要指定高度,整个组件高度默认继承父元素100%,所以在页面使用tabs组件时父元素必须设置高度。

(2)禁用手势滑动切换

swiper组件有个属性disable-touch用来禁用轮播图触摸操作,该属性只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、抖音小程序与飞书小程序。微信小程序可以通过@touchmove.prevent阻止触摸事件冒泡来阻止页面滑动

代码实现:

<template>
	<view class="comp-container">
		<view class="tabs">
			<!-- 标签栏 -->
			<view :class="['tab',{active:index==selectIndex}]" v-for="(item,index) in  list" :key="index"
				@click="handleSelect(index)">{{item}}</view>
			<!-- 下划线 -->
			<view :class="['underline',{transition:left!==null}]"></view>
		</view>
		<!-- 内容区域 -->
		<view class="content">
			<swiper class="swiper" :current="selectIndex" disable-touch @change="onSwiperChange">
				<swiper-item class="swiper-item" v-for="item in list" :key="item" @touchmove.prevent>
					<scroll-view scroll-y style="height:100%">
						<!-- 页面内容 -->
						<view class="main">{{item}}</view>
					</scroll-view>
				</swiper-item>
			</swiper>
		</view>
	</view>
</template>

运行效果:
请添加图片描述

(3)内容区域换为插槽

内容区域通过动态插槽提供给页面自定义渲染

核心代码如下:

	<!-- 内容区域 -->
		<view class="content">
			<swiper class="swiper" :current="selectIndex" @change="onSwiperChange">
				<swiper-item class="swiper-item" v-for="(item,index) in list" :key="index">
					<scroll-view scroll-y style="height:100%">
						<!-- 页面内容 -->
			             <!-- #ifdef H5 ||APP -->
						<slot :name="`content${index}`"></slot>
						<!--#endif -->
						<!-- #ifdef MP-WEIXIN -->
						<slot name="content{{index}}"></slot>
						<!--#endif -->
					</scroll-view>
				</swiper-item>
			</swiper>
		</view>

需要注意的是微信小程序端不支持vue插槽动态命名语法,需要写成双花括号形式。

完整代码:
tabs.vue

<template>
	<view class="comp-container">
		<view class="tabs">
			<!-- 标签栏 -->
			<view :class="['tab',{active:index==selectIndex}]" v-for="(item,index) in  list" :key="index"
				@click="handleSelect(index)">{{item}}</view>
			<!-- 下划线 -->
			<view :class="['underline',{transition:left!==null}]"></view>
		</view>
		<!-- 内容区域 -->
		<view class="content">
			<swiper class="swiper" :current="selectIndex" @change="onSwiperChange">
				<swiper-item class="swiper-item" v-for="(item,index) in list" :key="index">
					<scroll-view scroll-y style="height:100%">
						<!-- 页面内容 -->
			             <!-- #ifdef H5 ||APP -->
						<slot :name="`content${index}`"></slot>
						<!--#endif -->
						<!-- #ifdef MP-WEIXIN -->
						<slot name="content{{index}}"></slot>
						<!--#endif -->
					</scroll-view>
				</swiper-item>
			</swiper>
		</view>
	</view>
</template>

<script setup>
	import {
		computed,
		ref,
		onMounted,
		nextTick,
		getCurrentInstance
	} from 'vue'
	const props = defineProps({
		//标签配置
		list: {
			type: Array,
			default: () => []
		},
		//激活颜色
		activeColor: {
			type: String,
			defalut: 'deepskyblue'
		}
	})

	//激活颜色
	const activeColor = computed(() => {
		return props.activeColor || 'deepskyblue'
	})

	//当前选中索引
	const selectIndex = ref(0)

	//切换标签
	const handleSelect = (index) => {
		if (index !== selectIndex.value) {
			selectIndex.value = index
			setPosition()
		}
	}

	//下划线离父元素左边距
	const left = ref(null)
	//组件实例
	const instance = getCurrentInstance()
	//设置下划线位置
	const setPosition = () => {
		nextTick(() => {
			let query = uni.createSelectorQuery().in(instance.proxy)
			query.select(".active").boundingClientRect(data => {
				//定位距离=选中标签项与左距离+标签宽一半
				left.value = `${data.left+data.width/2}px`

			}).exec()
		})
	}

	onMounted(() => {
		//设置下划线初始位置
		setPosition()
	})
	
	
	//手势切换回调
	const onSwiperChange = e => {
		if (e.detail.current !== selectIndex.value) {
			handleSelect(e.detail.current)
		}
	}
</script>

<style lang="scss" scoped>
	.comp-container {
		height: 100%;
		display: flex;
		flex-direction: column;
		background-color: #f2f2f2;
		overflow: hidden;
	}
	.tabs {
		width: 100%;
		display: flex;
		align-items: center;
		background-color: #fff;
		position: relative;
		flex-shrink: 0;

		.tab {
			flex: 1;
			padding: 25rpx 10rpx;
			width: 0;
			box-sizing: border-box;
			text-align: center;
			white-space: nowrap;
			overflow: hidden;
			text-overflow: ellipsis;
			color: #333;
			font-size: 30rpx;
			position: relative;

			&.active {
				font-weight: bold;
				color: v-bind(activeColor);
			}
		}
	}

	//下划线
	.underline {
		position: absolute;
		width: 30px;
		height: 6rpx;
		background-color: v-bind(activeColor);
		border-radius: 6rpx;
		bottom: 0;
		left: v-bind(left);
		transform: translateX(-50%);
		display: none;

		&.transition {
			display: block;
			transition: all 0.3s;
		}
	}

	.content {
		flex: 1;
		height: 0;
		overflow: hidden;

		.swiper {
			height: 100%;

			.swiper-item {
				height: 100%;
			}
		}

		.main {
			background: #f2f2f2;
			text-align: center;
			padding: 30rpx;
			box-sizing: border-box;
		}
	}
</style>

页面调用:
index.vue

<template>
	<view class="container">
		<Tabs :list="list" >
			<template #content0>手机</template>
			<template #content1>电脑</template>
			<template #content2>电视机</template>
			<template #content3>洗衣机</template>
		</Tabs>
	</view>
</template>

<script setup>
	import Tabs from '@/components/tabs.vue'
	import {
		ref
	} from 'vue'
	const list = ref(['手机', '电脑', '电视机', '洗衣机'])
</script>

<style lang="scss" scoped>
	.container {
		height: 100vh;
		background-color: #f2f2f2;
	}
</style>


四、标签栏可滚动

最终版——功能继续升级,上述案例都是基于标签比较少的场景下使用,如果标签很多超出屏幕就不再适用,此时标签栏需要支持滚动。
我们目标不仅支持滚动,更友好操作体验还希望实现点击某个标签自动移动到屏幕中间,如下图所示:

请添加图片描述

实现说明:

1、横向滚动:
布局上标签栏外层使用scroll-view包裹,内层依然使用flex布局,每个标签设置基础宽度,当标签过多总体宽度超出屏幕就出现横向滚动。

2、选中标签移动到屏幕中间:
需要分别计算滚动条位置和下划线位置

(1)滚动条位置
scroll-view 有个scroll-left属性控制滚动条位置,我们只需计算该值即可。
在这里插入图片描述

如上图所示,假设点击了热水器标签,热水器标签要移动到屏幕中间,需要平移a段距离,a=c+b/2,其中b为标签自身宽度,c为标签距离页面左边距离-页面宽/2,最终滚动条scrollLeft值=原scrollLeft值+a,所以每次标签切换都要记录计算scrollLeft值,scrollLeft初始值为
0。上述几个值都可以通过节点信息api获取。
需要注意的是如果点击第一或第二个标签或最后一个标签情况是无法使得标签移动到正中间,因为滚动条长度有限,所以在计算scrollLeft值时候需要限制最大值最小值。最小值为0,最大值为滚动条长度-页面宽度,而滚动条长度可以通过如下api获取:

query.select('#scrollview').fields(
                   {
                     size: true,
					scrollOffset: true
					},
					(data) => {
					console.log(data.scrollWidth,'滚动条长度')

				})

(2)下划线位置
下划线还是和之前的案例一样基于父容器绝对定位,因为滚动条的出现使得父容器不在位于页面最左边,而是滚动条最左边,所以下划线位置还需加上滚动条滚出左边页面区域长度也即scrollLeft值

代码实现

tabs.vue

<template>
	<view class="comp-container">
		<view class="tabs">
			<!-- 标签栏 -->
			<scroll-view id="scrollview" :scroll-left="scrollLeft" scroll-x style="width:100%" scroll-with-animation>
				<view class="title-wrap" >
					<view :class="[selectedIndex===index ?'active':'','title-item']" v-for="(item,index) in list"
						:key="item" @click="onSelect(index)">{{item}}</view>
						<!-- 下划线 -->
					<view :class="['underline',{transition:left!==null}]"></view>
				</view>
			</scroll-view>
		</view>
		<!-- 内容区域 -->
		<view class="content">
			<swiper class="swiper" :current="selectedIndex" @change="onSwiperChange" >
				<swiper-item class="swiper-item" v-for="(item,index) in list" :key="index">
					<scroll-view scroll-y style="height:100%">
						<!-- 页面内容 -->
						<!-- #ifdef H5 ||APP -->
						<slot :name="`content${index}`"></slot>
						<!--#endif -->
						<!-- #ifdef MP-WEIXIN -->
						<slot name="content{{index}}"></slot>
						<!--#endif -->
					</scroll-view>
				</swiper-item>
			</swiper>
		</view>

	</view>
</template>

<script setup>
	import {
		ref,
		onMounted,
		getCurrentInstance,
		nextTick,
		computed
	} from 'vue'
	
	const props = defineProps({
		//标签配置
		list: {
			type: Array,
			default: () => []
		},
		//激活颜色
		activeColor: {
			type: String,
			defalut: 'deepskyblue'
		}
	})
	
	//当前选中索引
	const selectedIndex = ref(0)
	//下划线离父元素左边距
	const left = ref(null)
	const instance = getCurrentInstance()

	//窗口宽度
	const winWidth = uni.getSystemInfoSync().windowWidth

	//设置滚动条和下划线位置
	const setPosition = () => {
		nextTick(() => {
			let query = uni.createSelectorQuery().in(instance.proxy)
			query.select(".active").boundingClientRect(async data => {
				
				//获取滚动条节点信息
				let scrollViewInfo= await getScrollViewInfo();
				//重新获取滚动条scrollLeft值,防止用户手动触发滚动情况下值scrollLeft未及时更新
				scrollLeft.value=scrollViewInfo.scrollLeft
				
				let offsetLeft = data.left
				let offsetWidth = data.width
				//设置下划线位置
				left.value = ((offsetLeft + offsetWidth / 2) + scrollLeft.value) + 'px'
				//计算滚动条位置
				let _scrollLeft=scrollLeft.value+ data.left + offsetWidth / 2 - winWidth / 2
				//限制滚动范围
				_scrollLeft = Math.max(0, _scrollLeft)
				//设置滚动条位置
				scrollLeft.value = Math.min(_scrollLeft, scrollWidth.value - winWidth)
			}).exec()
		})

	}

	//选中标签监听事件
	const onSelect = index => {
		if (index !== selectedIndex.value) {
			selectedIndex.value = index;
			setPosition()
		}

	}

	//手势切换回调
	const onSwiperChange = e => {
		if (e.detail.current !== selectedIndex.value) {
			onSelect(e.detail.current)
		}
	}

	//滚动条位置
	const scrollLeft = ref(0)
	//滚动条长度
	const scrollWidth = ref(0)
	//获取滚动条长度和位置信息
	const getScrollViewInfo = () => {
		return new Promise((resolve, reject) => {
			let query = uni.createSelectorQuery().in(instance.proxy)
			query.select('#scrollview').fields({
					size: true,
					scrollOffset: true,
				},
				(data) => {
					resolve({scrollWidth:data.scrollWidth,scrollLeft:data.scrollLeft})
				}
			).exec()
		})

	}

	onMounted(() => {
		nextTick(async () => {
			//初始化化记录滚动条长度
			let res= await getScrollViewInfo();
			scrollWidth.value=res.scrollWidth
			
			setPosition()
		})

	})
</script>

<style lang="scss" scoped>
	:deep(::-webkit-scrollbar) {
		display: none;
	}

	.comp-container {
		height: 100%;
		display: flex;
		flex-direction: column;
		background-color: #f2f2f2;
		overflow: hidden;
	}

	.tabs {
		width: 100%;
		position: relative;
		flex-shrink: 0;
		background-color: #fff;

		.title-wrap {
			width: 100%;
			display: flex;
			align-items: center;
			box-sizing: border-box;
			justify-content: flex-start;
			position: relative;
		

			.title-item {
				padding: 25rpx 10rpx;
				flex: 1 0 22%;
				width: 0;
				box-sizing: border-box;
				text-align: center;
				white-space: nowrap;
				overflow: hidden;
				text-overflow: ellipsis;

				&.active {
					color: deepskyblue;
					font-weight: bold;
				}
			}
		}

		.underline {
			position: absolute;
			width: 30px;
			height: 6rpx;
			background-color: deepskyblue;
			border-radius: 6rpx;
			bottom: 0;
			left: v-bind(left);
			transform: translateX(-50%);
			display: none;

			&.transition {
				display: block;
				transition: all 0.3s;
			}
		}
	}

	.content {
		flex: 1;
		height: 0;
		overflow: hidden;

		.swiper {
			height: 100%;

			.swiper-item {
				height: 100%;
			}
		}

		.main {
			background: #f2f2f2;
			padding: 30rpx;
			text-align: center;
		}
	}
</style>

页面调用
index.vue

<template>
	<view class="container">
		<Tabs :list="list">
			<template #content0>手机</template>
			<template #content1>电脑</template>
			<template #content2>电视机</template>
			<template #content3>洗衣机</template>
			<template #content4>洗碗机</template>
			<template #content5>热水器</template>
			<template #content6>电冰箱</template>
			<template #content7>烤箱</template>
		</Tabs>
	</view>
</template>

<script setup>
	import Tabs from '@/components/tabs.vue'
	import {
		ref
	} from 'vue'
	const list = ref(['手机', '电脑', '电视机', '洗衣机', '洗碗机', '热水器', '电冰箱', '烤箱'])
</script>

<style lang="scss" scoped>
	.container {
		height: 100vh;
		background-color: #f2f2f2;
	}
</style>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

五、docker的网络模式

五、docker的网络模式 5.1 Docker的四种网络模式 当你安装docker时&#xff0c;它会自动创建三个网络&#xff0c;可使用如下命令查看&#xff1a; [rootlocalhost ~]# docker network ls NETWORK ID NAME DRIVER SCOPE 7390284b02d6 bridge bridge lo…

生产慎用之调试日志对空间矢量数据批量插入的性能影响-以MybatisPlus为例

目录 前言 一、一些缘由 1、性能分析 二、插入方式调整 1、批量插入的实现 2、MP的批量插入实现 3、日志的配置 三、默认处理方式 1、基础程序代码 2、执行情况 四、提升调试日志等级 1、在logback中进行设置 2、提升后的效果 五、总结 前言 在现代软件开发中&…

【人工智能】深度解剖利用人工智能MSA模型

目录 情感分析的应用一、概述二、研究背景三、主要贡献四、模型结构和代码五、数据集介绍六、性能展示七、复现过程 情感分析的应用 近年来社交媒体的空前发展以及配备高质量摄像头的智能手机的出现&#xff0c;我们见证了多模态数据的爆炸性增长&#xff0c;如电影、短视频等…

MongoDB性能监控工具

mongostat mongostat是MongoDB自带的监控工具&#xff0c;其可以提供数据库节点或者整个集群当前的状态视图。该功能的设计非常类似于Linux系统中的vmstat命令&#xff0c;可以呈现出实时的状态变化。不同的是&#xff0c;mongostat所监视的对象是数据库进程。mongostat常用于…

Scratch教学作品 | 中国诗词大会——闯关擂台,品味诗词之美! ✨

&#x1f393; Scratch教学作品 | 中国诗词大会——闯关擂台&#xff0c;品味诗词之美&#xff01; &#x1f4dc;✨ 今天给大家推荐一款结合文化与挑战的Scratch作品——《中国诗词大会》&#xff01;由zhouyq制作&#xff0c;这款游戏让你置身诗词的世界&#xff0c;通过闯关…

安全关系型数据库查询新选择:Rust 语言的 rust-query 库深度解析

在当今这个数据驱动的时代&#xff0c;数据库作为信息存储和检索的核心组件&#xff0c;其重要性不言而喻。然而&#xff0c;对于开发者而言&#xff0c;如何在保证数据安全的前提下&#xff0c;高效地进行数据库操作却是一项挑战。传统的 SQL 查询虽然强大&#xff0c;但存在诸…

微信小程序里的小游戏研发需要什么技术栈

研发小程序里的小游戏通常需要以下技术栈&#xff1a; 前端技术 HTML5 / CSS3&#xff1a;用于构建游戏的界面布局和样式。JavaScript&#xff1a;作为核心编程语言&#xff0c;实现游戏的逻辑和交互。小程序开发框架&#xff1a;如微信小程序的开发框架&#xff0c;了解其 API…

Install PyTorch (安装 PyTorch)

Install PyTorch {安装 PyTorch} 1. Install PyTorch1.1. Previous PyTorch Versions1.2. Latest PyTorch1.3. 查看 PyTorch 的版本 References 1. Install PyTorch https://pytorch.org/ Select your preferences and run the install command. Stable represents the most …

第二篇:k8s工作流程

我们来看通过deployment部署pod的常规流程&#xff1a; kubectl向apiserver发送部署请求&#xff08;例如使用 kubectl create -f deployment.yml&#xff09;apiserver将 Deployment 持久化到etcd&#xff1b;etcd与apiserver进行一次http通信。controller manager通过watch a…

智创 AI 新视界 -- 优化 AI 模型训练效率的策略与技巧(16 - 1)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

第五节、电机多段运动【51单片机-TB6600驱动器-步进电机教程】

摘要&#xff1a;本节介绍用控制步进电机三个主要参数角度、速度、方向&#xff0c;实现简单的步进电机多段控制 一、目标功能 输入多个目标角度&#xff0c;设定好步进电机速度&#xff0c;实现步进电机多段转动 二、计算过程 2.1 速度计算 根据第三节内容&#xff0c;定时器…

C++(九)

前言&#xff1a; 本文主要讲述运算符的优先顺序。 一&#xff0c;运算符的优先级。 请看以下表达式&#xff1a; a32*5 运算结果为&#xff1a;13. 可以看到&#xff0c;在此代码中&#xff0c;先运行了2*5的结果&#xff0c;在此基础上在进行3操作&#xff0c;因此结果…

志愿服务管理系统设计与实现

私信我获取源码和万字论文&#xff0c;制作不易&#xff0c;感谢点赞支持。 摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0…

dbeaver安装

数据库常用的管理工具就是navicat&#xff0c;页面简洁大方&#xff0c;且易上手&#xff0c;唯一不好的就是要收费&#xff0c;个人使用的话可以用dbeaver&#xff0c;一款开源的数据库管理工具。 下载地址&#xff1a;https://dbeaver.io/download/ 直接下载这个windows(inst…

Odoo :一款免费且开源的食品生鲜领域ERP管理系统

文 / 贝思纳斯 Odoo金牌合作伙伴 引言 提供业财人资税的精益化管理&#xff0c;实现研产供销的融通、食品安全的追踪与溯源&#xff0c;达成渠道的扁平化以及直面消费者的 D2C 等数字化解决方案&#xff0c;以此提升运营效率与核心竞争力&#xff0c;支撑高质量的变速扩张。…

【AIGC半月报】AIGC大模型启元:2024.12(上)

【AIGC半月报】AIGC大模型启元&#xff1a;2024.12&#xff08;上&#xff09; &#xff08;1&#xff09;OpenAI-12日发布会&#xff08;持续更新中........&#xff09;Day01-12.06&#xff1a;o1满血版上线&#xff08;已发布&#xff09;Day02-12.07&#xff1a;强化微调&a…

Mysql学习-Mysql查询(1)

1.基本查询&#xff08;SELECT&#xff09; SELECT语句基本格式&#xff1a; SELECT {*|<字段列表>} [ FROM<表1>&#xff0c;<表2>.. [WHERE <表达式> [GROUP BY<group by definition>] [HAVING <expression>[{<operator><exp…

OpenCV-平滑图像

二维卷积(图像滤波) 与一维信号一样&#xff0c;图像也可以通过各种低通滤波器&#xff08;LPF&#xff09;、高通滤波器&#xff08;HPF&#xff09;等进行过滤。LPF 有助于消除噪音、模糊图像等。HPF 滤波器有助于在图像中找到边缘。 opencv 提供了函数 **cv.filter2D()**&…

WPS解决Word文件引入excel对象文件无法打开提示“不能启动此对象...”的问题

一、问题现象 接收到了一份 Word文件&#xff0c;里面引入了一个Excel对象文件&#xff0c;双击时候&#xff0c;wps出现卡顿&#xff0c;过一会之后弹出错误提示&#xff1a;不能启动此对象... 二、解决方法 1.点击WPS左上角图标&#xff0c;并打开右上角设置&#xff0c;萱蕚…

做异端中的异端 -- Emacs裸奔之路6: 不可能存在的跳转功能

当一个问题存在两难时&#xff0c;市面上就不太可能出现稳定的&#xff0c;大众化的解决方案。 这很多是一个哲学问题 两害权衡&#xff0c; 存在很强的个性差异. 这种问题需要自己解决&#xff0c; 这个就是为什么要使用Emacs或者Vim的原因。 今天分享的一个想法&#xff0…