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

组件功能概述

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

  • 分步引导:支持多步骤引导流程
  • 智能定位:自动计算高亮区域位置
  • 遮罩效果:突出显示目标元素
  • 方向感知:根据位置调整提示框方向
  • 进度控制:下一步/跳过/完成操作
  • 状态保存:使用 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 实现新手引导访问功能组件的更多相关文章

  1. 七、vue语法补充二(动态组件 & 异步组件、访问元素 & 组件、混入)

    1..sync 修饰符 2.3.0+ 新增 vue 修饰符sync的功能是:当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定.类似于v-model的效果 例子: this.$ ...

  2. Visual Studio 2012出现“无法访问T-SQL组件和安装了不兼容伯 DacFx版本”的解决办法

    参考:Visual Studio 2012出现“无法访问T-SQL组件和安装了不兼容伯 DacFx版本”的解决办法 Vs2012的下载地址: https://msdn.microsoft.com/en ...

  3. 在VirtualBox中安装了Ubuntu后,Ubuntu的屏幕分辨率非常小,操作非常不便。通过安装VirtualBox提供的“增强功能组件”,-摘自网络

    在VirtualBox中安装了Ubuntu后,Ubuntu的屏幕分辨率非常小,操作非常不便.通过安装VirtualBox提供的“增强功能组件”,可以解决这一问题,并且使用非常方便. 一.环境 | En ...

  4. DSAPI多功能组件编程应用-HTTP监听服务端与客户端_指令版

    前面介绍了DSAPI多功能组件编程应用-HTTP监听服务端与客户端的内容,这里介绍一个适用于更高效更快速的基于HTTP监听的服务端.客户端. 在本篇,你将见到前所未有的超简化超傻瓜式的HTTP监听服务 ...

  5. DSAPI多功能组件编程应用-参考-Win32API常数

    DSAPI多功能组件编程应用-参考-Win32API常数 在编程过程中,常常需要使用Win32API来实现一些特定功能,而Win32API又往往需要使用一些API常数,百度搜索常数值,查手册,也就成了 ...

  6. DSAPI多功能组件编程应用-网络相关(上)

    [DSAPI.DLL下载地址]  DSAPI多功能组件编程应用-网络相关,网络相关编程有很多很多,这里讲解一下封装在DSAPI中的网络相关的功能,这些都是本人简化到极点的功能了,可以在软件开发过程中节 ...

  7. IIS Asp.Net 访问 Com组件 报拒绝访问

    IIS Asp.Net 访问 Com组件 报拒绝访问 解决方法: IIS 程序池->高级设置->进程模式->标识->内置帐户=LocalSystem

  8. 怎么关闭win10快速访问功能?关闭Windows10系统快速访问方法

    怎么关闭win10快速访问功能?关闭Windows10系统快速访问方法 Windows10系统的"快速访问"功能很容易泄露电脑中的隐私,用什么方法可以让这个功能消失,避免电脑的个人 ...

  9. 兼容性强、简单、成熟、稳定的RTMPClient客户端拉流功能组件EasyRTMPClient

    EasyRTMPClient EasyRTMPClient拉流功能组件是EasyDarwin流媒体团队开发.提供和维护的一套非常稳定.易用.支持重连的RTMPClient工具,SDK形式提供,全平台支 ...

  10. Vue父子组件传值之——访问根组件$root、$parent、$children和$refs

    Vue组件传值除了prop和$emit,我们还可以直接获取组件对象: 根组件: $root // 单一对象 表示当前组件树的根 Vue 实例,即new Vue({...根组件内容}).如果当前实例没有 ...

随机推荐

  1. 「硬核实战」回调函数到底是个啥?一文带你从原理到实战彻底掌握C/C++回调函数

    大家好,我是小康. 网上讲回调函数的文章不少,但大多浅尝辄止.缺少系统性,更别提实战场景和踩坑指南了.作为一个在生产环境中与回调函数打了多年交道的开发者,今天我想分享一些真正实用的经验,带你揭开回调函 ...

  2. JavaScript Library – Embla Carousel

    前言 2022 年 4 月,我写了一篇 Swiper 介绍. Swiper 是当时前端最多人使用的 Slider 库,没有之一,一骑绝尘. 但是!时过境迁,这两年已经有一匹神秘的黑马悄悄杀上来了. 它 ...

  3. 通过 Python 在PDF中添加、或删除超链接

    PDF文件现已成为文档存储和分发的首选格式.然而,PDF文件的静态特性有时会限制其交互性.超链接是提高PDF文件互动性和用户体验的关键元素.Python作为一种强大的编程语言,拥有多种库和工具来处理P ...

  4. 为什么不推荐在 MySQL 中直接存储图片、音频、视频等大容量内容?

    在MySQL中直接存储图片.音频.视频等大容量内容(通常称为BLOB数据)通常不被推荐,主要原因包括以下几点: 1. 性能问题 存储效率:存储大容量文件(如图片.音频.视频等)会大幅增加数据库的存储负 ...

  5. 我与 ChatGPT 讨论了面向对象语言 中,关于动态调用的问题

    你好,支持面向对象的语言中,"方法表" 是用来处理什么的? 在面向对象的语言中,"方法表"通常指一个类或对象中定义的方法列表.这些方法定义了该类或对象可以做什么 ...

  6. C# 线程基础——用户模式、内核模式

    C# 线程基础--用户模式.内核模式 参照:用户模式和内核模式 - Windows drivers | Microsoft Docs 基础概念 运行 Windows 的计算机中的处理器有两个不同模式: ...

  7. crypto14解题思路

    crypto14解题思路 ##二进制 001100110011001100100000001101000011010100100000001101010011000000100000001100100 ...

  8. CF1930G Prefix Max Set Counting 题解

    题意: 给定一棵以 1 为根的有根树,求出其所有 dfs 序中前缀最大值序列的数量.\(n\le 10^6\). 思路 显然考虑 DP. 由于是求前缀最大值序列的方案数,因此如果一些点要出现在这个序列 ...

  9. K8stools工具

    简介 K8stools 是一个 Kubernetes 日常运维辅助工具集,旨在提升运维效率,辅助平台治理与资源优化.功能涵盖资源分析.趋势评估.异常检测.行为采集.成本估算等常见场景,适用于 DevO ...

  10. C#中扩展方法无法获得多态性的行为

    在C#中,扩展方法(Extension Methods)是一种用于给现有类型添加新方法的技术.但是,扩展方法无法实现多态性的行为,因为它们是静态方法,它们的行为是在编译时确定的,而不是在运行时. 多态 ...