Uniapp 实现新手引导访问功能组件

最近有个需求需要在小程序中实现一个新手引导组件,通过遮罩、高亮区域和提示框的组合,为应用提供流畅的用户引导体验。

组件功能概述

这个引导组件提供了以下核心功能:

  • 分步引导:支持多步骤引导流程
  • 智能定位:自动计算高亮区域位置
  • 遮罩效果:突出显示目标元素
  • 方向感知:根据位置调整提示框方向
  • 进度控制:下一步/跳过/完成操作
  • 状态保存:使用 localStorage 记录完成状态(已取消,可扩展)

核心实现代码分析

组件模板结构

<template>   <view v-if="visible" class="guide-mask">     <!-- 遮罩四块 -->     <view class="mask-piece top" :style="maskStyles.top"></view>     <view class="mask-piece bottom" :style="maskStyles.bottom"></view>     <view class="mask-piece left" :style="maskStyles.left"></view>     <view class="mask-piece right" :style="maskStyles.right"></view>      <!-- 高亮区域 -->     <view       v-if="currentStep"       class="highlight"       :style="highlightStyleStr"     ></view>      <!-- 提示框 -->     <view class="tooltip" :style="tooltipStyleStr">       <text class="tip-text">{{ currentStep.tip }}</text>       <view class="tip-arrow" :class="currentStep.tipPosition || 'left'"></view>       <view class="btns">         <button @tap="nextStep">{{ isLast ? "完成" : "下一步" }}</button>         <button class="skip" @tap="skip">跳过</button>       </view>     </view>      <!-- 引导机器人图标:这个也可以是别的图标,这边用的是机器人图标 -->     <image       class="robot-img"       src="更换为自己的图标"       :style="tooltipStyleImg"       mode="widthFix"     />   </view> </template> 

组件逻辑实现

export default {   props: {     steps: { type: Array, required: true }, // 引导步骤配置     guideKey: { type: String, default: "default_guide_key" }, // 引导标识键,用于确认是否做过引导,可以扩展   },   data() {     return {       stepIndex: 0, // 当前步骤索引       visible: false, // 是否显示引导     };   },   computed: {     // 当前步骤配置     currentStep() {       return this.steps[this.stepIndex];     },      // 是否为最后一步     isLast() {       return this.stepIndex === this.steps.length - 1;     },      // 高亮区域样式     highlightStyleStr() {       // 计算样式逻辑...     },      // 机器人图标位置     tooltipStyleImg() {       // 根据提示位置计算坐标...     },      // 提示框样式     tooltipStyleStr() {       // 根据位置计算提示框方向...     },      // 遮罩层样式计算     maskStyles() {       // 计算四块遮罩的位置和尺寸...     },   },   methods: {     // 开始引导     start(force = false) {       if (!force) return;       this.stepIndex = 0;       this.visible = true;     },      // 下一步     nextStep() {       this.isLast ? this.finish() : this.stepIndex++;     },      // 跳过引导     skip() {       this.finish();     },      // 完成引导     finish() {       this.visible = false;       this.$emit("finish");       localStorage.setItem(this.guideKey, "completed");     },   }, }; 

样式实现

.guide-mask {   position: fixed;   top: 0;   left: 0;   width: 100vw;   height: 100vh;   z-index: 1000001; }  .highlight {   position: absolute;   border: 2px solid #fff;   border-radius: 8px;   box-shadow: 0 0 10px #fff; }  .tooltip {   position: absolute;   background: white;   padding: 10px 16px;   border-radius: 12px;   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);   min-width: 250rpx; }  /* 箭头方向样式 */ .tip-arrow.bottom {   bottom: -8px;   border-left: 8px solid transparent;   border-right: 8px solid transparent;   border-top: 8px solid white; }  /* 其他方向样式... */  .robot-img {   position: absolute;   width: 150rpx;   z-index: 10003; } 

关键实现技术

1. 智能遮罩计算

组件将遮罩分为四个部分(上、下、左、右),通过计算目标元素的位置动态设置每块遮罩的尺寸:

maskStyles() { 	const { top, left, width, height } = this.currentStep 	const windowWidth = uni.getSystemInfoSync().windowWidth 	const windowHeight = uni.getSystemInfoSync().windowHeight  	return { 		top: `... height: ${top}px; ...`, 		bottom: `... top: ${top + height}px; height: ${windowHeight - (top + height)}px; ...`, 		left: `... top: ${top}px; width: ${left}px; height: ${height}px; ...`, 		right: `... left: ${left + width}px; width: ${windowWidth - (left + width)}px; ...` 	} } 

2. 动态提示框定位

根据目标元素位置自动调整提示框方向:

tooltipStyleStr() { 	const top = this.currentStep.top + this.currentStep.height + 10 	const left = this.currentStep.left 	const right = this.currentStep.right 	const tipPosition = this.currentStep.tipPosition || 'left' 	const { windowWidth } = uni.getSystemInfoSync();  	return tipPosition === 'left' 		? `right:${windowWidth - right}px;` 		: `left:${left}px;` } 

3. 引导机器人位置计算

根据提示方向计算机器人图标位置:

tooltipStyleImg() { 	const { top, left, width, height, tipPosition = 'left' } = this.currentStep 	let x = 0, y = 0  	switch (tipPosition) { 		case 'left': 			x = left - width 			y = top + height 			break 		case 'right': 			x = left + width / 5 * 3 			y = top + height 			break 		// 其他情况... 	}  	return `top:${y}px;left:${x}px;` } 

完整代码

<template>   <view v-if="visible" class="guide-mask">     <!-- 遮罩四块 -->     <view class="mask-piece top" :style="maskStyles.top"></view>     <view class="mask-piece bottom" :style="maskStyles.bottom"></view>     <view class="mask-piece left" :style="maskStyles.left"></view>     <view class="mask-piece right" :style="maskStyles.right"></view>      <view       v-if="currentStep"       class="highlight"       :style="highlightStyleStr"     ></view>      <view class="tooltip" :style="tooltipStyleStr">       <text class="tip-text">{{ currentStep.tip }}</text>       <view class="tip-arrow" :class="currentStep.tipPosition || 'left'"></view>       <view class="btns">         <button @tap="nextStep">{{ isLast ? "完成" : "下一步" }}</button>         <button class="skip" @tap="skip">跳过</button>       </view>     </view>      <image       class="robot-img"       src="@/images/static/robot.png"       :style="tooltipStyleImg"       mode="widthFix"     />   </view> </template>  <script> export default {   props: {     steps: {       type: Array,       required: true,     },     guideKey: {       type: String,       default: "default_guide_key",     },   },   data() {     return {       stepIndex: 0,       visible: false,     };   },   computed: {     currentStep() {       return this.steps[this.stepIndex];     },     isLast() {       return this.stepIndex === this.steps.length - 1;     },     highlightStyleStr() {       if (!this.currentStep) return "";       const { top, left, width, height } = this.currentStep;       return `position:absolute;top:${top}px;left:${left}px;width:${width}px;height:${height}px;border:2px solid #fff;box-shadow:0 0 10px #fff;border-radius:8px;z-index:10000;`;     },     tooltipStyleImg() {       if (!this.currentStep) return "";       const {         top,         left,         width,         height,         tipPosition = "left",       } = this.currentStep;       let x = 0,         y = 0;       switch (tipPosition) {         case "left":           x = left - width;           y = top + height;           break;         case "right":           x = left + (width / 5) * 3;           y = top + height;           break;         case "top":           x = left + width / 2;           y = top - 100; // 高度预估           break;         case "bottom":         default:           x = left + width / 2;           y = top + height;           break;       }        return `top:${y}px;left:${x}px;`;     },     tooltipStyleStr() {       if (!this.currentStep) return "";       const top = this.currentStep.top + this.currentStep.height + 10;       const left = this.currentStep.left;       const right = this.currentStep.right;       const tipPosition = this.currentStep.tipPosition || "left";       const { windowWidth } = uni.getSystemInfoSync();       return (         `position:absolute;top:${top}px;z-index:10001;` +         (tipPosition === "left"           ? `right:${windowWidth - right}px;`           : `left:${left}px;`)       );     },     maskStyles() {       if (!this.currentStep) return {};        const { top, left, width, height } = this.currentStep;       const windowWidth = uni.getSystemInfoSync().windowWidth;       const windowHeight = uni.getSystemInfoSync().windowHeight;        return {         top: `position: absolute; top: 0px; left: 0px; width: ${windowWidth}px; height: ${top}px; background: rgba(0, 0, 0, 0.6);`,         bottom: `position: absolute; top: ${           top + height         }px; left: 0px; width: ${windowWidth}px; height: ${           windowHeight - (top + height)         }px; background: rgba(0, 0, 0, 0.6);`,         left: `position: absolute; top: ${top}px; left: 0px; width: ${left}px; height: ${height}px; background: rgba(0, 0, 0, 0.6);`,         right: `position: absolute; top: ${top}px; left: ${           left + width         }px; width: ${           windowWidth - (left + width)         }px; height: ${height}px; background: rgba(0, 0, 0, 0.6);`,       };     },   },   methods: {     start(force = false) {       if (!force) return;       this.stepIndex = 0;       this.visible = true;     },     nextStep() {       if (this.isLast) {         this.finish();       } else {         this.stepIndex++;       }     },     skip() {       this.finish();     },     finish() {       this.visible = false;       this.$emit("finish");     },   }, }; </script>  <style scoped> .guide-mask {   position: fixed;   top: 0;   left: 0;   width: 100vw;   height: 100vh;   z-index: 1000001; }  .mask-piece {   position: absolute;   background: rgba(0, 0, 0, 0.6); }  .mask-layer {   background: rgba(0, 0, 0, 0.6);   width: 100%;   height: 100%;   position: absolute; }  .highlight {   position: absolute;   border: 2px solid #fff;   border-radius: 8px;   box-shadow: 0 0 10px #fff; }  .tooltip {   /* box-shadow: 0 0 8px #0004; */   position: absolute;   background: white;   padding: 10px 16px;   border-radius: 12px;   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);   font-size: 14px;   color: #007aff;   z-index: 10002;   min-width: 250rpx; }  .tip-arrow {   position: absolute;   width: 0;   height: 0; }  .tip-arrow.bottom {   bottom: -8px;   left: 20px;   border-left: 8px solid transparent;   border-right: 8px solid transparent;   border-top: 8px solid white; }  .tip-arrow.top {   top: -8px;   left: 20px;   border-left: 8px solid transparent;   border-right: 8px solid transparent;   border-bottom: 8px solid white; }  .tip-arrow.right {   top: 12px;   right: -8px;   border-top: 8px solid transparent;   border-bottom: 8px solid transparent;   border-left: 8px solid white; }  .tip-arrow.left {   top: 12px;   left: -8px;   border-top: 8px solid transparent;   border-bottom: 8px solid transparent;   border-right: 8px solid white; }  .robot-img {   position: absolute;   width: 150rpx;   z-index: 10003; }  .tip-text {   font-size: 14px;   color: #333; }  .btns {   display: flex;   gap: 10px; }  button {   font-size: 12px;   padding: 4px 8px; }  .skip {   color: #888; } </style> 

使用示例

const guideSteps = [ 	{ 		tip: "这是 AI 聊天功能,点击进行聊天", 		top: 100, 		left: 50, 		width: 200, 		height: 40, 		tipPosition: "bottom" 	}, 	{ 		tip: "这是个人中心入口", 		top: 500, 		left: 300, 		width: 80, 		height: 80, 		tipPosition: "left" 	} ]  // 在组件中使用 <GuideMask :steps="guideSteps" guideKey="home_guide" @finish="onGuideFinish"/> 

组件不足

  1. tip&icon 定位:这里的组件定位主要是做了左右适配定位,如果需要兼容可以进行扩展或者优化
  2. 高亮区域:组件高亮区域当前只是对于定位区域宽高进行高亮,可以做往外扩展,例如椭圆形的
  3. 跨页面:目前只能对同个单一的页面进行引导式访问,无法做到跨页面跳转的引导式访问
  4. 多端适配:暂无进行多端的适配测试,目前看来应该兼容的,实用还是得做下测试进行优化
  5. 遮罩层:这里遮罩层做的是根据定位区域来实现覆盖的,没有进行穿透效果,兼容可以好点,但也可以进行其他方面的优化例如各种形状或者区域高亮扩展,这时候就需要更复杂的计算,扩展性维护性就差点

优化方向

不足的地方都可以进行优化,下面就只是扩展方向:

  1. 动画效果:为高亮区域和提示框添加过渡动画
  2. 自动定位:通过选择器自动获取元素位置(使用 createSelectorQuery 和 boundingClientRect)
  3. 主题定制:支持自定义颜色和样式
  4. 手势支持:添加滑动手势切换步骤
  5. 语音引导:结合语音 API 提供语音提示
  6. 引导记忆:组件有个标识专门针对已经做过引导访问的页面进行标识,如果遇到可以不再引导,也可以强制引导

总结

这个只是做了简单的示例,有需要可以进行优化改善,没有太大要求的话可以直接复制粘贴使用。效果图片想想还是贴下吧:
Uniapp 实现新手引导访问功能组件

发表评论

评论已关闭。

相关文章

当前内容话题