Uniapp 實現(xiàn)新手引導(dǎo)訪問功能組件
最近有個需求需要在小程序中實現(xiàn)一個新手引導(dǎo)組件,通過遮罩、高亮區(qū)域和提示框的組合,為應(yīng)用提供流暢的用戶引導(dǎo)體驗。
組件功能概述
這個引導(dǎo)組件提供了以下核心功能:
- 分步引導(dǎo):支持多步驟引導(dǎo)流程
- 智能定位:自動計算高亮區(qū)域位置
- 遮罩效果:突出顯示目標(biāo)元素
- 方向感知:根據(jù)位置調(diào)整提示框方向
- 進(jìn)度控制:下一步/跳過/完成操作
- 狀態(tài)保存:使用 localStorage 記錄完成狀態(tài)(已取消,可擴展)
核心實現(xiàn)代碼分析
組件模板結(jié)構(gòu)
<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>
<!-- 高亮區(qū)域 -->
<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>
<!-- 引導(dǎo)機器人圖標(biāo):這個也可以是別的圖標(biāo),這邊用的是機器人圖標(biāo) -->
<image
class="robot-img"
src="更換為自己的圖標(biāo)"
:style="tooltipStyleImg"
mode="widthFix"
/>
</view>
</template>
組件邏輯實現(xiàn)
export default {
props: {
steps: { type: Array, required: true }, // 引導(dǎo)步驟配置
guideKey: { type: String, default: "default_guide_key" }, // 引導(dǎo)標(biāo)識鍵,用于確認(rèn)是否做過引導(dǎo),可以擴展
},
data() {
return {
stepIndex: 0, // 當(dāng)前步驟索引
visible: false, // 是否顯示引導(dǎo)
};
},
computed: {
// 當(dāng)前步驟配置
currentStep() {
return this.steps[this.stepIndex];
},
// 是否為最后一步
isLast() {
return this.stepIndex === this.steps.length - 1;
},
// 高亮區(qū)域樣式
highlightStyleStr() {
// 計算樣式邏輯...
},
// 機器人圖標(biāo)位置
tooltipStyleImg() {
// 根據(jù)提示位置計算坐標(biāo)...
},
// 提示框樣式
tooltipStyleStr() {
// 根據(jù)位置計算提示框方向...
},
// 遮罩層樣式計算
maskStyles() {
// 計算四塊遮罩的位置和尺寸...
},
},
methods: {
// 開始引導(dǎo)
start(force = false) {
if (!force) return;
this.stepIndex = 0;
this.visible = true;
},
// 下一步
nextStep() {
this.isLast ? this.finish() : this.stepIndex++;
},
// 跳過引導(dǎo)
skip() {
this.finish();
},
// 完成引導(dǎo)
finish() {
this.visible = false;
this.$emit("finish");
localStorage.setItem(this.guideKey, "completed");
},
},
};
樣式實現(xiàn)
.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;
}
關(guān)鍵實現(xiàn)技術(shù)
1. 智能遮罩計算
組件將遮罩分為四個部分(上、下、左、右),通過計算目標(biāo)元素的位置動態(tài)設(shè)置每塊遮罩的尺寸:
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. 動態(tài)提示框定位
根據(jù)目標(biāo)元素位置自動調(diào)整提示框方向:
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. 引導(dǎo)機器人位置計算
根據(jù)提示方向計算機器人圖標(biāo)位置:
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;`
}
完整代碼
<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; // 高度預(yù)估
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>
使用示例
const guideSteps = [
{
tip: "這是 AI 聊天功能,點擊進(jìn)行聊天",
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"/>
組件不足
- tip&icon 定位:這里的組件定位主要是做了左右適配定位,如果需要兼容可以進(jìn)行擴展或者優(yōu)化
- 高亮區(qū)域:組件高亮區(qū)域當(dāng)前只是對于定位區(qū)域?qū)捀哌M(jìn)行高亮,可以做往外擴展,例如橢圓形的
- 跨頁面:目前只能對同個單一的頁面進(jìn)行引導(dǎo)式訪問,無法做到跨頁面跳轉(zhuǎn)的引導(dǎo)式訪問
- 多端適配:暫無進(jìn)行多端的適配測試,目前看來應(yīng)該兼容的,實用還是得做下測試進(jìn)行優(yōu)化
- 遮罩層:這里遮罩層做的是根據(jù)定位區(qū)域來實現(xiàn)覆蓋的,沒有進(jìn)行穿透效果,兼容可以好點,但也可以進(jìn)行其他方面的優(yōu)化例如各種形狀或者區(qū)域高亮擴展,這時候就需要更復(fù)雜的計算,擴展性維護(hù)性就差點
優(yōu)化方向
不足的地方都可以進(jìn)行優(yōu)化,下面就只是擴展方向:
- 動畫效果:為高亮區(qū)域和提示框添加過渡動畫
- 自動定位:通過選擇器自動獲取元素位置(使用 createSelectorQuery 和 boundingClientRect)
- 主題定制:支持自定義顏色和樣式
- 手勢支持:添加滑動手勢切換步驟
- 語音引導(dǎo):結(jié)合語音 API 提供語音提示
- 引導(dǎo)記憶:組件有個標(biāo)識專門針對已經(jīng)做過引導(dǎo)訪問的頁面進(jìn)行標(biāo)識,如果遇到可以不再引導(dǎo),也可以強制引導(dǎo)
總結(jié)
這個只是做了簡單的示例,有需要可以進(jìn)行優(yōu)化改善,沒有太大要求的話可以直接復(fù)制粘貼使用。效果圖片想想還是貼下吧:


浙公網(wǎng)安備 33010602011771號