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

文章目录

  • 前言
  • 一、简单版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>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

相对路径和绝对路径与链接标签

一.相对路径 相对路径&#xff1a;以引用文件所在位置为参考基础&#xff0c;而建立出的目录路径。 即图片相对于你写的html页面的位置 相对路径分类符号说明同一级路径图片与html文件处于同一级&#xff0c;如<img src"baidu.gif">下一级路径/图片位于html…

【Java】Switch语句、循环语句(for、while、do...while)

Switch语句&#xff1a;针对某个表达式的值进行判断&#xff0c;从而决定执行哪一段代码 语法格式&#xff1a; switch(表达式){ case 目标值1: 执行语句1 break; case 目标值2: …

P3916 图的遍历(Tarjan缩点和反向建边)

P3916 图的遍历 - 洛谷 | 计算机科学教育新生态 写法一&#xff1a;Tarjan 思路&#xff1a;先运用Tarjan算法得到每个连通块中最大的编号&#xff0c;然后对每个连通块进行缩点重新建图&#xff0c;进行dfs&#xff0c;得到缩点后的连通块能够达到的最大编号。 Code: conste…

2024年认证杯SPSSPRO杯数学建模D题(第一阶段)AI绘画带来的挑战解题全过程文档及程序

2024年认证杯SPSSPRO杯数学建模 D题 AI绘画带来的挑战 原题再现&#xff1a; 2023 年开年&#xff0c;ChatGPT 作为一款聊天型AI工具&#xff0c;成为了超越疫情的热门词条&#xff1b;而在AI的另一个分支——绘图领域&#xff0c;一款名为Midjourney&#xff08;MJ&#xff…

同为科技(TOWE)柔性定制化PDU插座

随着科技的进步&#xff0c;越来越多的精密电子设备&#xff0c;成为工作生活密不可分的工具。 电子电气设备的用电环境也变得更为复杂&#xff0c;所以安全稳定的供电是电子电气设备的生命线。 插座插排作为电子电气设备最后十米范围内供配电最终核心部分&#xff0c;便捷、安…

GPS模块/SATES-ST91Z8LR:电路搭建;直接用电脑的USB转串口进行通讯;模组上报定位数据转换地图识别的坐标手动查询地图位置

从事嵌入式单片机的工作算是符合我个人兴趣爱好的,当面对一个新的芯片我即想把芯片尽快搞懂完成项目赚钱,也想着能够把自己遇到的坑和注意事项记录下来,即方便自己后面查阅也可以分享给大家,这是一种冲动,但是这个或许并不是原厂希望的,尽管这样有可能会牺牲一些时间也有哪天原…

设计模式阅读笔记

参考&#xff1a;设计模式目录&#xff1a;22种设计模式 设计模式是什么&#xff1f; 设计模式是软件设计中常见问题的典型解决方案。 它们就像能根据需求进行调整的预制蓝图&#xff0c; 可用于解决代码中反复出现的设计问题。 设计模式与方法或库的使用方式不同&#xff0c…

详尽的oracle sql函数

1&#xff0c;CHR 输入整数&#xff0c;返回对应字符。 用法&#xff1a;select chr(65),chr(78) from dual; 2&#xff0c;ASCII 输入字符&#xff0c;返回对应ASCII码。 用法&#xff1a;select ascii(A),ascii(B) from dual; 3&#xff0c;CONCAT 输入两个字符串&#xff0c…

C++小碗菜之二:软件单元测试

“没有测试的代码重构不能称之为重构&#xff0c;它仅仅是垃圾代码的到处移动” ——Corey Haines 目录 前言 什么是单元测试&#xff1f; 单元测试的组成 单元测试的命名 单元测试的独立性 Google Test 单元测试的环境配置与使用 1. Ubuntu下安装 Google Test 2. 编写…

Go 1.19.4 HTTP编程-Day 20

1. HTTP协议 1.1 基本介绍 HTTP协议又称超文本传输协议&#xff0c;属于应用层协议&#xff0c;在传输层使用TCP协议。HTTP协议属是无状态的&#xff0c;对事务处理没有记忆能力&#xff0c;如果需要保存状态需要引用其他技术&#xff0c;如Cookie。HTTP协议属是无连接的&…

【SpringBoot】使用IDEA创建SpringBoot项目

1、使用SpringBoot脚手架创建 我们使用SpringBoot的脚手架Spring Initializr创建&#xff0c;如图所示&#xff1a; 2、选择SpringBoot版本 最开始做项目时候&#xff0c;组长说创建一个 springboot 2.5.4 的项目&#xff0c;mysql使用 5.6.X &#xff0c;maven使用是3.6.X…

使用Oracle通过gateway连接MSSQL

环境概述 某医院的his系统Oracle数据库要和体检系统进行数据通讯&#xff0c;需要从Oracle能查到sqlserver的数据。本次通过Oracle gateway来解决此问题。 HIS服务器&#xff1a;windows server 2016数据库oracle11.2.0.4&#xff0c;假设IP是192.168.100.9 体检服务器&…

leetcode 之 二分查找(java)(2)

文章目录 74、搜索二维矩阵33、搜素旋转排序数组 74、搜索二维矩阵 题目描述&#xff1a; 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff…

Linux中的信号

目录 生活中的信号 Linux中的信号 前台进程与后台进程 信号的产生 核心转储 core dump ​编辑信号的其他相关概念 信号处理的三种方式 信号在内核中的表示示意图 sigset_t 类型 信号集操作函数 sigprocmask sigpending 综合练习 用户态与内核态 信号的捕捉过程 …

基于STM32F4实现步进电机闭环控制实现(无PID)

文章目录 概要整体流程代码实现TIM8 PWM控制TIM5 编码器计数TIM13 闭环控制 效果展示小结 概要 因客户外部负载较大&#xff0c;步进电机出现丢步现象&#xff0c;所以需要进行闭环控制&#xff0c;保证最后走到相应的位置即可&#xff0c;所以我采用的是电机停止后与编码器值…

第4章:颜色和背景 --[CSS零基础入门]

在 CSS 中&#xff0c;颜色和背景属性是用于美化网页元素的重要工具。你可以通过多种方式定义颜色&#xff0c;并且可以设置元素的背景颜色、图像、渐变等。以下是关于如何在 CSS 中使用颜色和背景的一些关键点和示例。 1.颜色表示法 当然&#xff01;以下是使用不同颜色表示…

二叉树概述

目录 一、二叉树的基本结构 二、二叉树的遍历 1.前序 2.中序 3.后序 4.层序遍历 三.计算二叉树的相关参数 1.计算节点总个数 2.计算叶子节点的个数 3.计算树的高度 4.计算第k层的子树个数 5.查找树中val为x的节点 四.刷题 1.单值二叉树 2.检查两棵树是否相同 3.一…

04 创建一个属于爬虫的主虚拟环境

文章目录 回顾conda常用指令创建一个爬虫虚拟主环境Win R 调出终端查看当前conda的虚拟环境创建 spider_base 的虚拟环境安装完成查看环境是否存在 为 pycharm 配置创建的爬虫主虚拟环境选一个盘符来存储之后学习所写的爬虫文件用 pycharm 打开创建的文件夹pycharm 配置解释器…

weblogic开启https

JSK证书生成 生成密钥库和证书 使用Java的keytool命令来生成一个Java密钥库&#xff08;Keystore&#xff09;和证书。keytool是Java开发工具包&#xff08;JDK&#xff09;中用于管理密钥库和证书的命令行工具。 #创建证书存放目录 [weblogicosb1 jksHL]$ mkdir -p /home/w…

学习记录,正则表达式, 隐式转换

正则表达式 \\&#xff1a;表示正则表达式 W: 表示一个非字&#xff08;不是一个字&#xff0c;例如&#xff1a;空格&#xff0c;逗号&#xff0c;句号&#xff09; W: 多个非字 基本组成部分 1.字符字面量&#xff1a; 普通字符&#xff1a;在正则表达式中&#xff0c;大…