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

总结

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

相关推荐
wearegogog1237 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars7 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤7 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·7 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°7 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
qq_419854058 小时前
CSS动效
前端·javascript·css
烛阴8 小时前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪8 小时前
markstream-vue实战踩坑笔记
前端
C_心欲无痕8 小时前
nginx - 实现域名跳转的几种方式
运维·前端·nginx
花哥码天下9 小时前
恢复网站console.log的脚本
前端·javascript·vue.js