一、需求背景
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‘类名即可