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

发布于:2025-06-19 ⋅ 阅读:(10) ⋅ 点赞:(0)

一、需求背景

1、需要实现小程序新功能引导
2、不使用第三方库(第三方组件试了几个,都是各种兼容性问题,放弃)

二、实现步骤

1、写一个公共的guide组件,代码如下
components/Guide/index.tsx文件

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文件

.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‘类名即可

最终效果

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到