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

总结

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

相关推荐
90后的晨仔7 小时前
在macOS上无缝整合:为Claude Code配置魔搭社区免费API完全指南
前端
沿着路走到底7 小时前
JS事件循环
java·前端·javascript
子春一28 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶8 小时前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn9 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪9 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ9 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied9 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一29 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
QT 小鲜肉10 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记