taro小程序如何实现新用户引导功能?

一、需求背景

1、需要实现小程序新功能引导

2、不使用第三方库(第三方组件试了几个,都是各种兼容性问题,放弃)

二、实现步骤

1、写一个公共的guide组件,代码如下

components/Guide/index.tsx文件

bash 复制代码
import React, { useEffect, useState } from "react";
import Taro from "@tarojs/taro";
import { View, Button } from "@tarojs/components";
import { AtCurtain } from "taro-ui";

import "./index.less";

interface Props {
  // 需要指引的整体元素,会对此块增加一个整体的蒙层,直接用guild元素包裹即可
  children: React.ReactNode;
  // 指引的具体dom
  guildList: {
    content: string; // 指引内容
    id: string; // 指引的id  --eg:  'test'
  }[];

  // 是否默认对被引导的dom添加高亮,只对子元素的子元素动态添加类名【cur-guide】
  isAuto?: boolean;
  // 1、此字段只对isAuto为false时生效
  // 2、部分页面,需传入此字段微调整距离顶部的距离
  otherHeight?: number;
  // 此字段只对isAuto为false时生效
  // activeGuide值变化时触发,用来在isAuto为false时,告知外部需要高亮哪个dom,请外部根据此判断添加类明【cur-guide】
  onChange?: (activeGuideId) => void;
}

interface TaroElementProps {
  className?: string;
  children?: React.ReactNode;
  props: {
    id: string;
    [key: string]: any;
  };
}
type TaroElement = React.ReactElement<TaroElementProps>;

const Guide = (props: Props) => {
  const [isOpened, setIsOpened] = useState(true);
  const [activeGuide, setActiveGuide] = useState(0);
  const [tipPosition, setTipPosition] = useState({
    top: 0,
    left: 0,
  });

  useEffect(() => {
    if (!props.isAuto) {
      updatePosition();
      props.onChange?.(props.guildList[activeGuide]?.id);
    }
  }, [activeGuide]);

  const updatePosition = () => {
    Taro.nextTick(() => {
      if (!props.guildList[activeGuide]) return;
      const query = Taro.createSelectorQuery();
      query
        .select(`#${props.guildList[activeGuide].id}`)
        .boundingClientRect()
        .selectViewport()
        .scrollOffset()
        .exec((res) => {
          if (res && res[0] && res[1]) {
            // res[0] 是元素的位置信息
            // res[1] 是页面滚动的位置信息
            const rect = res[0];
            const scrollTop = res[1].scrollTop;

            // 计算元素距离顶部的实际距离(包含滚动距离)
            const actualTop = rect.top + scrollTop;
            setTipPosition({
              top:
                actualTop +
                rect.height -
                (props.otherHeight || 0) +
                12,
              left: rect.left + rect.width / 2,
            });
          }
        });
    });
  };
  const onPre = () => {
    if (activeGuide <= 0) {
      setActiveGuide(0);
      setIsOpened(false);
      return;
    }
    setActiveGuide(activeGuide - 1);
  };
  const onNext = () => {
    if (activeGuide >= props.guildList.length - 1) {
      setActiveGuide(props.guildList.length - 1);
      setIsOpened(false);
      return;
    }
    setActiveGuide(activeGuide + 1);
  };

  const renderTip = () => {
    return (
      <View
        className="cur-guide-tip"
        style={{
          top: `${tipPosition.top}px`,
          left: `${tipPosition.left}px`,
        }}
      >
        <Button onClick={onPre}>上一步</Button>
        <Button onClick={onNext}>下一步</Button>
      </View>
    );
  };
  // 递归处理子元素,找到对应index的元素添加提示内容
  const enhanceChildren = (children: React.ReactNode) => {
    return React.Children.map(children, (child) => {
      if (!React.isValidElement(child)) return child;

      // 如果当前元素是数组(比如map渲染的列表),需要特殊处理
      if (child.props.children) {
        // 处理子元素
        const enhancedChildren = React.Children.map(
          child.props.children,
          (subChild) => {
            if (!React.isValidElement(subChild)) return subChild;

            const subChildProps = (subChild as TaroElement).props as any;
            const isCurrentActive =
              subChildProps.id === props.guildList[activeGuide]?.id;

            // 如果是当前激活的索引,为其添加提示内容
            if (isCurrentActive && isOpened) {
              const subChildProps = (subChild as TaroElement).props;
              return React.cloneElement(subChild as TaroElement, {
                className: `${subChildProps.className || ""} ${
                  isCurrentActive ? "cur-guide" : ""
                }`,
                children: [
                  ...(Array.isArray(subChildProps.children)
                    ? subChildProps.children
                    : [subChildProps.children]),
                  renderTip(),
                ],
              });
            }
            return subChild;
          }
        );

        return React.cloneElement(child as TaroElement, {
          ...child.props,
          children: enhancedChildren,
        });
      }

      return child;
    });
  };

  const renderBody = () => {
    return (
      <>
        <View>{props.children}</View>
        {isOpened && renderTip()}
      </>
    );
  };
  const renderBodyAuto = () => {
    return <View>{enhanceChildren(props.children)}</View>;
  };
  return (
    <View className="fc-guide">
      {props.isAuto ? renderBodyAuto() : renderBody()}
      {isOpened && (
        <AtCurtain isOpened={isOpened} onClose={() => {}}></AtCurtain>
      )}
    </View>
  );
};
export default Guide;

components/Guide/index.less文件

bash 复制代码
.fc-guide {
  position: relative;
  .at-curtain {
    z-index: 20;
    .at-curtain__btn-close {
      display: none;
    }
  }

  // 这个是相对顶部距离的定位(isAuto为false时)
  .cur-guide-tip {
    padding: 24px;
    background-color: #fff;
    position: absolute;
    z-index: 22;
    transform: translate(-50%, 0);
  }
  // 相对当前高亮元素的定位(isAuto为true时)
  .cur-guide {
    background: #f5f5f5;
    position: relative;
    z-index: 22;
    .cur-guide-tip {
      bottom: 0 !important;
      left: 50% !important;
      transform: translate(-50%, 100% + 12px);
    }
  }
}

2、使用方式
a.isAuto为true时的传值结构

b.isAuto为false时

需要配合onChange事件将当前激活id传给父组件,然后父组件再根据当前激活id去选择高亮哪个dom元素(类名判断写在和id设置同一个dom上),然后给对应dom绑上'cur-guide'类名即可

最终效果

相关推荐
流***陌13 小时前
一键预约上门服务:到家洗车小程序的便捷功能与场景化体验
小程序
说私域13 小时前
整合与超越:论“开源AI智能名片链动2+1模式S2B2C商城小程序”对传统红人直播带货模式的升维
人工智能·小程序
说私域13 小时前
定制开发开源AI智能名片S2B2C商城小程序新手引导教程的重要性与实施方法研究
人工智能·小程序·开源
上海云盾商务经理杨杨13 小时前
2025年电商小程序小量DDoS攻击防护指南:从小流量到大威胁的全面防护方案
网络安全·小程序·ddos
刘晓倩13 小时前
实战练习:小程序页面间跳转传值 & 子页面数据渲染
小程序
老师可可14 小时前
Excel学生成绩表,如何生成成绩分析报告?
经验分享·学习·小程序·excel·学习方法
巧夺噶事来15 小时前
培训班小程序模板如何一键套用,分享微信小程序的制作方法
微信小程序·小程序
2501_9159214316 小时前
Charles 抓包 HTTPS 原理详解,从 CONNECT 到 SSL Proxying、常见问题与真机调试实战(含 Sniffmaster 补充方案)
android·网络协议·小程序·https·uni-app·iphone·ssl
OEC小胖胖16 小时前
组件化思维(上):视图与基础内容组件的深度探索
微信·微信小程序·小程序·微信开放平台