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

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

组件功能概述

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

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

核心实现代码分析

组件模板结构

vue 复制代码
<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>

组件逻辑实现

javascript 复制代码
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");
    },
  },
};

样式实现

css 复制代码
.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. 智能遮罩计算

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

javascript 复制代码
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. 动态提示框定位

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

javascript 复制代码
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. 引导机器人位置计算

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

javascript 复制代码
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;`
}

完整代码

vue 复制代码
<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>

使用示例

javascript 复制代码
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. 引导记忆:组件有个标识专门针对已经做过引导访问的页面进行标识,如果遇到可以不再引导,也可以强制引导

总结

这个只是做了简单的示例,有需要可以进行优化改善,没有太大要求的话可以直接复制粘贴使用。

相关推荐
RadiumAg40 分钟前
记一道有趣的面试题
前端·javascript
yangzhi_emo1 小时前
ES6笔记2
开发语言·前端·javascript
yanlele1 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
中微子2 小时前
React状态管理最佳实践
前端
烛阴2 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子2 小时前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端
Hexene...3 小时前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts
天天扭码3 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html
xw53 小时前
我犯了错,我于是为我的uni-app项目引入环境标志
前端·uni-app
!win !3 小时前
被老板怼后,我为uni-app项目引入环境标志
前端·小程序·uni-app