mirror of
https://gitee.com/wot-design-uni/wot-design-uni.git
synced 2025-12-06 09:08:51 +08:00
feat: ✨ 新增 tour 漫游组件
This commit is contained in:
parent
e185a8b291
commit
4aaf257166
@ -241,6 +241,10 @@ export default defineConfig({
|
||||
{
|
||||
link: '/en-US/component/index-bar',
|
||||
text: 'IndexBar'
|
||||
},
|
||||
{
|
||||
link: '/en-US/component/tour',
|
||||
text: 'Tour'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -248,6 +248,9 @@ export default defineConfig({
|
||||
{
|
||||
link: '/component/index-bar',
|
||||
text: 'IndexBar 索引栏'
|
||||
},{
|
||||
link:'/component/tour',
|
||||
text: 'Tour 漫游'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
254
docs/component/tour.md
Normal file
254
docs/component/tour.md
Normal file
@ -0,0 +1,254 @@
|
||||
# Guide 漫游组件
|
||||
|
||||
用于引导用户逐步了解应用功能的组件,可以高亮显示页面中的特定元素并提供说明。
|
||||
|
||||
## 基本使用
|
||||
|
||||
通过 `steps` 属性设置引导步骤,通过 `v-model` 控制显示隐藏。
|
||||
|
||||
```html
|
||||
<template>
|
||||
<view>
|
||||
<view class="tour-item" id="step1">
|
||||
<text class="tour-title">第一步</text>
|
||||
<text class="tour-content">这是引导的第一步,介绍基本功能</text>
|
||||
</view>
|
||||
|
||||
<view class="tour-item" id="step2">
|
||||
<text class="tour-title">第二步</text>
|
||||
<text class="tour-content">这是引导的第二步,展示更多功能</text>
|
||||
</view>
|
||||
|
||||
<wd-tour
|
||||
:model-value="showTour"
|
||||
:steps="steps"
|
||||
v-model:current="current"
|
||||
@finish="onFinish"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showTour = ref(true)
|
||||
const current = ref(0)
|
||||
|
||||
const steps = [
|
||||
{
|
||||
element: '#step1',
|
||||
content: '这是第一步的说明'
|
||||
},
|
||||
{
|
||||
element: '#step2',
|
||||
content: '这是第二步的说明'
|
||||
}
|
||||
]
|
||||
|
||||
function onFinish() {
|
||||
console.log('引导完成')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 自定义引导内容
|
||||
|
||||
通过 `content` 插槽可以自定义引导内容。
|
||||
|
||||
```html
|
||||
<wd-tour :model-value="showTour" :steps="steps">
|
||||
<template #content>
|
||||
<view class="custom-content">
|
||||
<wd-icon name="help-circle-filled" size="22px"></wd-icon>
|
||||
<text class="custom-text">自定义引导内容区域</text>
|
||||
</view>
|
||||
</template>
|
||||
</wd-tour>
|
||||
```
|
||||
|
||||
### 自定义高亮区域
|
||||
|
||||
通过 `highlight` 插槽可以自定义高亮区域样式。
|
||||
|
||||
```html
|
||||
<wd-tour :model-value="showTour" :steps="steps">
|
||||
<template #highlight="{ elementInfo }">
|
||||
<view class="custom-highlight" :style="getCustomHighlightStyle(elementInfo)"></view>
|
||||
</template>
|
||||
</wd-tour>
|
||||
```
|
||||
|
||||
### 自定义按钮
|
||||
|
||||
通过 `prev`、`next`、`skip`、`finish` 插槽可以自定义按钮。
|
||||
|
||||
```html
|
||||
<wd-tour :model-value="showTour" :steps="steps" next-text="继续" finish-text="知道了">
|
||||
<template #next>
|
||||
<view class="custom-button custom-next">下一步</view>
|
||||
</template>
|
||||
<template #finish>
|
||||
<view class="custom-button custom-finish">完成</view>
|
||||
</template>
|
||||
</wd-tour>
|
||||
```
|
||||
|
||||
### 点击蒙版继续
|
||||
|
||||
通过 `click-mask-next` 属性可以设置点击蒙版是否可以下一步。
|
||||
|
||||
```html
|
||||
<wd-tour
|
||||
:model-value="showTour"
|
||||
:steps="steps"
|
||||
:click-mask-next="true"
|
||||
/>
|
||||
```
|
||||
|
||||
### 自定义蒙版样式
|
||||
|
||||
通过 `mask-color`、`offset`、`border-radius`、`padding` 属性可以自定义蒙版样式。
|
||||
|
||||
```html
|
||||
<wd-tour
|
||||
:model-value="showTour"
|
||||
:steps="steps"
|
||||
mask-color="rgba(255, 0, 0, 0.6)"
|
||||
:offset="40"
|
||||
:border-radius="15"
|
||||
:padding="20"
|
||||
/>
|
||||
```
|
||||
|
||||
### 关闭蒙版
|
||||
|
||||
通过 `mask` 属性可以控制是否显示蒙版。
|
||||
|
||||
```html
|
||||
<wd-tour
|
||||
:model-value="showTour"
|
||||
:steps="steps"
|
||||
:mask="false"
|
||||
/>
|
||||
```
|
||||
|
||||
### 控制当前步骤
|
||||
|
||||
通过 `v-model:current` 可以控制当前步骤。
|
||||
|
||||
```html
|
||||
<view>
|
||||
<wd-button @click="current = 2">跳转到第三步</wd-button>
|
||||
<wd-tour
|
||||
:model-value="showTour"
|
||||
:steps="steps"
|
||||
v-model:current="current"
|
||||
/>
|
||||
</view>
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|------|------|------|--------|--------|
|
||||
| v-model | 是否显示引导组件 | boolean | - | false |
|
||||
| steps | 引导步骤列表 | array | - | [] |
|
||||
| current | 当前步骤索引,支持 v-model:current 双向绑定 | number | - | 0 |
|
||||
| mask | 蒙版是否显示 | boolean | - | true |
|
||||
| mask-color | 蒙版颜色(支持 rgba 格式) | string | - | rgba(0, 0, 0, 0.5) |
|
||||
| offset | 引导提示框与高亮框的间距 | number | - | 20 |
|
||||
| duration | 动画持续时间(毫秒) | number | - | 300 |
|
||||
| border-radius | 高亮区域的圆角大小 | number | - | 8 |
|
||||
| padding | 高亮区域的内边距 | number | - | 10 |
|
||||
| prev-text | 上一步按钮文字 | string | - | 上一步 |
|
||||
| next-text | 下一步按钮文字 | string | - | 下一步 |
|
||||
| skip-text | 跳过按钮文字 | string | - | 跳过 |
|
||||
| finish-text | 完成按钮文字 | string | - | 完成 |
|
||||
| bottom-safety-offset | 底部安全偏移量,用于滚动计算时确保元素周围有足够的空间 | number | - | 100 |
|
||||
| top-safety-offset | 顶部安全偏移量,用于滚动计算时确保元素周围有足够的空间 | number | - | 0 |
|
||||
| custom-nav | 是否自定义顶部导航栏 | boolean | - | false |
|
||||
| click-mask-next | 点击蒙版是否可以下一步 | boolean | - | false |
|
||||
| z-index | 引导组件的层级 | number | - | 999998 |
|
||||
| show-tour-buttons | 是否显示引导按钮 | boolean | - | true |
|
||||
|
||||
## Steps 数据结构
|
||||
|
||||
| 属性 | 说明 | 类型 |
|
||||
|------|------|------|
|
||||
| element | 需要高亮的元素选择器 | string |
|
||||
| content | 引导文字内容(支持富文本) | string |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| change | 步骤改变时触发 | `{currentIndex: number }` |
|
||||
| prev | 点击上一步按钮时触发 | `{ oldCurrent: number, current: number, total: number, isUp: number }` |
|
||||
| next | 点击下一步按钮时触发 | `{ oldCurrent: number, current: number, total: number, isUp: number }` |
|
||||
| finish | 点击完成按钮时触发 | `{ current: number, total: number }` |
|
||||
| skip | 点击跳过按钮时触发 | `{ current: number, total: number }` |
|
||||
| error | 查找引导元素出错时触发 | `{ message: string, element: string }` |
|
||||
|
||||
## Slots
|
||||
|
||||
| 插槽名 | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| highlight | 自定义高亮区域 | elementInfo: 元素位置信息 |
|
||||
| content | 自定义引导内容 | - |
|
||||
| prev | 自定义上一步按钮 | - |
|
||||
| next | 自定义下一步按钮 | - |
|
||||
| skip | 自定义跳过按钮 | - |
|
||||
| finish | 自定义完成按钮 | - |
|
||||
|
||||
## Methods
|
||||
|
||||
通过 ref 可以获取到组件实例,调用组件提供的方法:
|
||||
|
||||
| 方法名 | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| handlePrev | 切换到上一步 | - |
|
||||
| handleNext | 切换到下一步 | - |
|
||||
| handleSkip | 跳过引导 | - |
|
||||
| handleFinish | 完成引导 | - |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保要高亮的元素在页面中存在且可选择
|
||||
2. 在自定义导航栏的情况下,需要设置 `custom-nav` 和适当的 `top-safety-offset` 值
|
||||
3. 引导组件会自动处理滚动,确保高亮元素在可视区域内
|
||||
4. 可以通过 `mask` 属性控制是否显示遮罩层
|
||||
5. 使用自定义高亮区域时,注意避免遮挡引导操作按钮
|
||||
6. 在不同平台(H5、微信小程序等)中,插槽的使用方式可能略有差异
|
||||
7. 建议在使用时添加 `v-if` 或 `v-show` 条件渲染,以确保组件正确初始化
|
||||
|
||||
## 主题定制
|
||||
|
||||
组件支持通过 CSS 变量定制主题,可以修改以下变量:
|
||||
|
||||
```scss
|
||||
// 蒙版颜色
|
||||
--wd-tour-mask-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
// 引导框背景色
|
||||
--wd-tour-popover-bg-color: #ffffff;
|
||||
|
||||
// 按钮背景色
|
||||
--wd-tour-button-primary-bg-color: #007aff;
|
||||
|
||||
// 按钮文字颜色
|
||||
--wd-tour-button-color: #ffffff;
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 为什么在微信小程序中自定义插槽不显示?
|
||||
|
||||
在微信小程序中使用条件渲染与插槽结合时可能会出现问题,建议使用 `v-show` 替代 `v-if` 或将条件判断移到插槽内部。
|
||||
|
||||
### 为什么高亮区域会闪烁?
|
||||
|
||||
这通常是因为组件初始化时元素信息尚未获取完成导致的。组件已优化初始状态,避免闪烁问题。
|
||||
|
||||
### 如何解决点击问题?
|
||||
|
||||
如果自定义高亮区域遮挡了操作按钮,可以调整 `z-index` 或修改自定义高亮区域的样式,确保按钮可点击。
|
||||
@ -985,6 +985,7 @@
|
||||
"ri-zhou-yue-qie-huan": "Day Week Month Switching",
|
||||
"ring-lei-xing-loading": "Ring type loading",
|
||||
"rootportal-title": "Root Portal",
|
||||
"tour-title": "Tour",
|
||||
"ru-ding-dan-chu-yu-zan-ting-zhuang-tai-jin-ru-wo-de-ding-dan-ye-mian-zhao-dao-yao-qu-xiao-de-ding-dan-dian-ji-qu-xiao-ding-dan-an-niu-xuan-ze-ding-dan-qu-xiao-yuan-yin-hou-dian-ji-xia-yi-bu-ti-jiao-shen-qing-ji-ke": "If the order is in a suspended state, go to the \"My Orders\" page, find the order you want to cancel, and click the \"Cancel Order\" button; After selecting the reason for canceling the order, click \"Next\" to submit the application.",
|
||||
"ru-ding-dan-chu-yu-zan-ting-zhuang-tai-jin-ru-wo-de-ding-dan-ye-mian-zhao-dao-yao-qu-xiao-de-ding-dan-dian-ji-qu-xiao-ding-dan-an-niu-xuan-ze-ding-dan-qu-xiao-yuan-yin-hou-dian-ji-xia-yi-bu-ti-jiao-shen-qing-ji-ke-0": "If the order is in a suspended state, go to the \"My Orders\" page, find the order you want to cancel, and click the \"Cancel Order\" button; After selecting the reason for canceling the order, click \"Next\" to submit the application.",
|
||||
"ruan-jian-gong-cheng": "software engineering",
|
||||
|
||||
@ -1175,6 +1175,7 @@
|
||||
"top-right": "top right",
|
||||
"tou-bi": "投币",
|
||||
"tou-xiang-gu-jia-ping": "头像骨架屏",
|
||||
"tour-title": "Tour 漫游",
|
||||
"transition-dong-hua": "Transition 动画",
|
||||
"transition-title": "Transition 动画",
|
||||
"tu-biao": "图标",
|
||||
|
||||
@ -1326,6 +1326,21 @@
|
||||
"navigationBarTitleText": "%rootportal-title%"
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "tour/Index",
|
||||
"name": "tour",
|
||||
"style": {
|
||||
"mp-alipay": {
|
||||
"allowsBounceVertical": "NO"
|
||||
},
|
||||
// #ifdef MP
|
||||
"navigationBarTitleText": "Tour 页面引导",
|
||||
// #endif
|
||||
// #ifndef MP
|
||||
"navigationBarTitleText": "%tour-title%"
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -143,6 +143,10 @@ const list = computed(() => [
|
||||
{
|
||||
id: 'indexBar',
|
||||
name: t('indexbar-suo-yin-lan')
|
||||
},
|
||||
{
|
||||
id: 'tour',
|
||||
name: t('tour-title')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
446
src/subPages/tour/Index.vue
Normal file
446
src/subPages/tour/Index.vue
Normal file
@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<page-wraper>
|
||||
<view class="tour-container">
|
||||
<view class="tour-step" id="step1">
|
||||
<view class="tour-item">
|
||||
<text class="tour-title">第一步</text>
|
||||
<text class="tour-content">这是引导的第一步,介绍基本功能</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tour-step" id="step2">
|
||||
<view class="tour-item">
|
||||
<text class="tour-title">第二步</text>
|
||||
<text class="tour-content">这是引导的第二步,展示更多功能</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tour-step" id="step3">
|
||||
<view class="tour-item">
|
||||
<text class="tour-title">第三步</text>
|
||||
<text class="tour-content">这是引导的第三步,深入功能介绍</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tour-step" id="step4">
|
||||
<view class="tour-item">
|
||||
<text class="tour-title">第四步</text>
|
||||
<text class="tour-content">这是引导的最后一步,总结功能</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本用法 -->
|
||||
<demo-block title="基本用法">
|
||||
<view class="button-group">
|
||||
<wd-button type="primary" @click="startBasicTour">开始引导</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 点击蒙版继续 -->
|
||||
<demo-block title="点击蒙版继续">
|
||||
<view class="button-group">
|
||||
<wd-button type="primary" @click="startMaskNextTour">点击蒙版继续</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 自定义蒙版 -->
|
||||
<demo-block title="自定义蒙版">
|
||||
<view class="button-group">
|
||||
<wd-button type="primary" @click="startCustomMaskTour">自定义蒙版</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 关闭蒙版 -->
|
||||
<demo-block title="关闭蒙版">
|
||||
<view class="button-group">
|
||||
<wd-button type="primary" @click="startNoMaskTour">关闭蒙版</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 自定义高亮区域 -->
|
||||
<demo-block title="自定义高亮区域">
|
||||
<view class="button-group">
|
||||
<wd-button type="primary" @click="startCustomHighlightTour">自定义高亮</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 自定义内容和按钮 -->
|
||||
<demo-block title="自定义内容和按钮">
|
||||
<view class="button-group">
|
||||
<wd-button type="primary" @click="startCustomContentTour">自定义内容</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 控制当前步骤 -->
|
||||
<demo-block title="控制当前步骤">
|
||||
<view class="button-group">
|
||||
<wd-button-group>
|
||||
<wd-button type="primary" @click="startControlTour">跳转到第三步开始引导</wd-button>
|
||||
</wd-button-group>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 基本用法组件 -->
|
||||
<wd-tour
|
||||
v-model="showBasicTour"
|
||||
:steps="basicSteps"
|
||||
v-model:current="current"
|
||||
:padding="10"
|
||||
@finish="handleFinish"
|
||||
@skip="handleSkip"
|
||||
@change="handleChange"
|
||||
></wd-tour>
|
||||
|
||||
<!-- 点击蒙版继续组件 -->
|
||||
<wd-tour
|
||||
v-model="showClickMaskTour"
|
||||
:steps="basicSteps"
|
||||
:click-mask-next="true"
|
||||
@finish="handleFinish"
|
||||
@skip="handleSkip"
|
||||
@change="handleChange"
|
||||
></wd-tour>
|
||||
|
||||
<!-- 自定义蒙版组件 -->
|
||||
<wd-tour
|
||||
v-model="showCustomMaskTour"
|
||||
:steps="customMaskSteps"
|
||||
:mask="true"
|
||||
mask-color="#7eb2f87d"
|
||||
:offset="40"
|
||||
:border-radius="15"
|
||||
:padding="10"
|
||||
:next-text="'下一步'"
|
||||
:prev-text="'上一步'"
|
||||
:skip-text="'跳过'"
|
||||
:finish-text="'完成'"
|
||||
@finish="handleFinish"
|
||||
@skip="handleSkip"
|
||||
@change="handleChange"
|
||||
></wd-tour>
|
||||
|
||||
<!-- 关闭蒙版组件 -->
|
||||
<wd-tour v-model="showNoMaskTour" :steps="noMaskSteps" :mask="false" @finish="handleFinish" @skip="handleSkip" @change="handleChange"></wd-tour>
|
||||
|
||||
<!-- 自定义高亮区域组件 -->
|
||||
<wd-tour
|
||||
v-model="showCustomHighlightTour"
|
||||
:steps="customHighlightSteps"
|
||||
:padding="10"
|
||||
@finish="handleFinish"
|
||||
@skip="handleSkip"
|
||||
@change="handleChange"
|
||||
>
|
||||
<template #highlight="{ elementInfo }">
|
||||
<view class="custom-highlight" :style="{ ...elementInfo, ...customHighlightStyle }"></view>
|
||||
</template>
|
||||
</wd-tour>
|
||||
|
||||
<!-- 自定义内容和按钮组件 -->
|
||||
<wd-tour
|
||||
v-model="showCustomContentTour"
|
||||
:steps="customContentSteps"
|
||||
:next-text="nextText"
|
||||
:prev-text="prevText"
|
||||
:skip-text="skipText"
|
||||
:finish-text="finishText"
|
||||
@finish="handleFinish"
|
||||
@skip="handleSkip"
|
||||
@change="handleChange"
|
||||
>
|
||||
<template #content>
|
||||
<view class="custom-content">
|
||||
<wd-icon name="help-circle-filled" size="22px"></wd-icon>
|
||||
<text class="custom-text">自定义引导内容区域</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template #next>
|
||||
<view class="custom-button custom-next">下一步</view>
|
||||
</template>
|
||||
|
||||
<template #finish>
|
||||
<view class="custom-button custom-finish">完成</view>
|
||||
</template>
|
||||
</wd-tour>
|
||||
|
||||
<!-- 控制当前步骤组件 -->
|
||||
<wd-tour
|
||||
v-model="showControlTour"
|
||||
:steps="basicSteps"
|
||||
v-model:current="controlCurrent"
|
||||
:padding="10"
|
||||
@finish="handleFinish"
|
||||
@skip="handleSkip"
|
||||
@change="handleChange"
|
||||
></wd-tour>
|
||||
</page-wraper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
|
||||
const showBasicTour = ref(false)
|
||||
const showClickMaskTour = ref(false)
|
||||
const showCustomMaskTour = ref(false)
|
||||
const showNoMaskTour = ref(false)
|
||||
const showCustomHighlightTour = ref(false)
|
||||
const showCustomContentTour = ref(false)
|
||||
const showControlTour = ref(false)
|
||||
const nextText = ref('继续')
|
||||
const prevText = ref('返回')
|
||||
const skipText = ref('跳过')
|
||||
const finishText = ref('知道了')
|
||||
const current = ref(0)
|
||||
const controlCurrent = ref(0)
|
||||
// 步骤数据
|
||||
const basicSteps = [
|
||||
{
|
||||
element: '#step1',
|
||||
content: '欢迎使用引导组件,这是第一步的说明'
|
||||
},
|
||||
{
|
||||
element: '#step2',
|
||||
content: '这是第二步,展示了另一个功能点'
|
||||
},
|
||||
{
|
||||
element: '#step3',
|
||||
content: '这里可以是<H1>富文本</H1>'
|
||||
},
|
||||
{
|
||||
element: '#step4',
|
||||
content: '这是最后一步,完成引导流程'
|
||||
}
|
||||
]
|
||||
|
||||
const customMaskSteps = [
|
||||
{
|
||||
element: '#step1',
|
||||
content: '这是<strong>自定义蒙版</strong>示例,使用了<strong>红色半透明</strong>蒙版'
|
||||
},
|
||||
{
|
||||
element: '#step2',
|
||||
content: '蒙版颜色设置为<code>rgba(255, 0, 0, 0.6)</code>'
|
||||
},
|
||||
{
|
||||
element: '#step3',
|
||||
content: '同时调整了<em>高亮区域的圆角</em>、<u>内边距</u>和<code>偏移量</code>'
|
||||
},
|
||||
{
|
||||
element: '#step4',
|
||||
content: '完成了自定义蒙版样式的展示'
|
||||
}
|
||||
]
|
||||
|
||||
const noMaskSteps = [
|
||||
{
|
||||
element: '#step1',
|
||||
content: '这是<strong>无蒙版</strong>引导模式'
|
||||
},
|
||||
{
|
||||
element: '#step2',
|
||||
content: '只高亮目标元素,<em>不显示</em>半透明遮罩'
|
||||
},
|
||||
{
|
||||
element: '#step3',
|
||||
content: '适用于需要保持页面可见性的场景'
|
||||
},
|
||||
{
|
||||
element: '#step4',
|
||||
content: '引导完成,<u>不干扰</u>用户查看页面其他内容'
|
||||
}
|
||||
]
|
||||
|
||||
const customHighlightSteps = [
|
||||
{
|
||||
element: '#step1',
|
||||
content: '这是<strong>自定义高亮区域</strong>示例'
|
||||
},
|
||||
{
|
||||
element: '#step2',
|
||||
content: '使用了<em>红色虚线边框</em>和<code>半透明背景</code>'
|
||||
},
|
||||
{
|
||||
element: '#step3',
|
||||
content: '通过插槽实现完全自定义的高亮样式'
|
||||
},
|
||||
{
|
||||
element: '#step4',
|
||||
content: '完成了自定义高亮区域的展示'
|
||||
}
|
||||
]
|
||||
|
||||
const customContentSteps = [
|
||||
{
|
||||
element: '#step1',
|
||||
content: '这是<H1>自定义样式</H1>的引导,可以修改<em>蒙版颜色</em>、<u>圆角大小</u>等属性'
|
||||
},
|
||||
{
|
||||
element: '#step2',
|
||||
content: '可以自定义按钮文字,如"继续"、"返回"等'
|
||||
},
|
||||
{
|
||||
element: '#step3',
|
||||
content: '通过属性配置实现个性化引导体验'
|
||||
},
|
||||
{
|
||||
element: '#step4',
|
||||
content: '这是最后一步,完成引导流程'
|
||||
}
|
||||
]
|
||||
// 自定义高亮样式
|
||||
const customHighlightStyle = {
|
||||
border: '2px dashed #ff0000',
|
||||
borderRadius: '8px',
|
||||
background: 'rgba(255, 0, 0, 0.1)',
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
|
||||
// 滑动到最上面
|
||||
async function scrollToTop() {
|
||||
await uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: 0
|
||||
})
|
||||
}
|
||||
// 启动不同类型引导的方法
|
||||
async function startBasicTour() {
|
||||
await scrollToTop()
|
||||
nextTick(() => {
|
||||
showBasicTour.value = true
|
||||
})
|
||||
}
|
||||
|
||||
function startMaskNextTour() {
|
||||
scrollToTop()
|
||||
showClickMaskTour.value = true
|
||||
}
|
||||
|
||||
async function startCustomMaskTour() {
|
||||
scrollToTop()
|
||||
showCustomMaskTour.value = true
|
||||
}
|
||||
|
||||
function startNoMaskTour() {
|
||||
scrollToTop()
|
||||
showNoMaskTour.value = true
|
||||
}
|
||||
|
||||
function startCustomHighlightTour() {
|
||||
scrollToTop()
|
||||
showCustomHighlightTour.value = true
|
||||
}
|
||||
|
||||
function startCustomContentTour() {
|
||||
scrollToTop()
|
||||
showCustomContentTour.value = true
|
||||
}
|
||||
|
||||
function startControlTour() {
|
||||
scrollToTop()
|
||||
controlCurrent.value = 2
|
||||
showControlTour.value = true
|
||||
}
|
||||
|
||||
// 通用事件处理
|
||||
function handleFinish() {
|
||||
console.log('引导完成')
|
||||
showBasicTour.value = false
|
||||
showClickMaskTour.value = false
|
||||
showCustomMaskTour.value = false
|
||||
showNoMaskTour.value = false
|
||||
showCustomHighlightTour.value = false
|
||||
showCustomContentTour.value = false
|
||||
showControlTour.value = false
|
||||
}
|
||||
|
||||
function handleSkip() {
|
||||
console.log('引导跳过')
|
||||
showBasicTour.value = false
|
||||
showClickMaskTour.value = false
|
||||
showCustomMaskTour.value = false
|
||||
showNoMaskTour.value = false
|
||||
showCustomHighlightTour.value = false
|
||||
showCustomContentTour.value = false
|
||||
showControlTour.value = false
|
||||
}
|
||||
|
||||
function handleChange(currentIndex: number) {
|
||||
console.log('当前步骤:', currentIndex)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tour-container {
|
||||
padding: 0;
|
||||
|
||||
.tour-step {
|
||||
width: fit-content;
|
||||
margin: 20px auto;
|
||||
|
||||
.tour-item {
|
||||
padding: 20px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
background-color: #f8f8f8;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
min-width: 250px;
|
||||
|
||||
.tour-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tour-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.custom-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
|
||||
.custom-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
&.custom-next {
|
||||
background-color: #34d19d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.custom-finish {
|
||||
background-color: #34d19d;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-highlight {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
123
src/uni_modules/wot-design-uni/components/wd-tour/index.scss
Normal file
123
src/uni_modules/wot-design-uni/components/wd-tour/index.scss
Normal file
@ -0,0 +1,123 @@
|
||||
@import './../common/abstracts/_mixin.scss';
|
||||
@import './../common/abstracts/variable.scss';
|
||||
|
||||
@include b(tour) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
|
||||
@include e(mask) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@include e(highlight) {
|
||||
position: fixed;
|
||||
background-color: transparent;
|
||||
box-sizing: content-box;
|
||||
animation: tour-show 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@include e(popover) {
|
||||
z-index: 10000;
|
||||
background-color: $-color-white;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
width: fit-content;
|
||||
min-width: 200px;
|
||||
animation: tour-show 0.3s ease;
|
||||
}
|
||||
|
||||
@include e(info) {
|
||||
font-size: 12px;
|
||||
background-color: $-color-white;
|
||||
border-radius: 16rpx;
|
||||
border: 1px solid $-color-white;
|
||||
color: $-color-black;
|
||||
width: fit-content;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@include e(buttons) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
@include e(prev) {
|
||||
@include e(default){
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(next) {
|
||||
@include e(default) {
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
background-color: $-button-primary-bg-color;
|
||||
color: $-color-white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(finish) {
|
||||
@include e(default) {
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
background-color: $-button-primary-bg-color;
|
||||
color: $-color-white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(skip) {
|
||||
|
||||
@include e(default){
|
||||
background-color: $-color-white;
|
||||
color: $-color-black;
|
||||
font-size: 12px;
|
||||
padding: 5px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tour-show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(tour) {
|
||||
@include e(popover) {
|
||||
background-color: $-dark-background2;
|
||||
}
|
||||
|
||||
@include e(info) {
|
||||
background-color: $-dark-background2;
|
||||
border-color: $-dark-background2;
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(skip) {
|
||||
background-color: $-dark-background2;
|
||||
color: $-dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/uni_modules/wot-design-uni/components/wd-tour/types.ts
Normal file
153
src/uni_modules/wot-design-uni/components/wd-tour/types.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import type { PropType } from 'vue'
|
||||
import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp, makeArrayProp } from '../common/props'
|
||||
|
||||
export interface TourStep {
|
||||
/**
|
||||
* 需要高亮的元素选择器
|
||||
*/
|
||||
element: string
|
||||
/**
|
||||
* 引导文字内容
|
||||
*/
|
||||
content: string
|
||||
}
|
||||
|
||||
export const tourProps = {
|
||||
...baseProps,
|
||||
|
||||
/**
|
||||
* 是否显示引导组件,使用 v-model 绑定
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
modelValue: makeBooleanProp(false),
|
||||
|
||||
/**
|
||||
* 引导步骤列表
|
||||
* 类型:array
|
||||
* 默认值:[]
|
||||
*/
|
||||
steps: makeArrayProp<TourStep>(),
|
||||
/**
|
||||
* 引导框的current
|
||||
* 类型:number
|
||||
* 默认值:0
|
||||
*/
|
||||
current: makeNumberProp(0),
|
||||
|
||||
/**
|
||||
* 蒙版是否显示
|
||||
* 类型:boolean
|
||||
* 默认值:true
|
||||
*/
|
||||
mask: makeBooleanProp(true),
|
||||
|
||||
/**
|
||||
* 蒙版颜色(支持 rgba 格式)
|
||||
* 类型:string
|
||||
* 默认值:'rgba(0, 0, 0, 0.5)'
|
||||
*/
|
||||
maskColor: makeStringProp('rgba(0, 0, 0, 0.5)'),
|
||||
|
||||
/**
|
||||
* 引导框与高亮元素之间的间距
|
||||
* 类型:number
|
||||
* 默认值:20
|
||||
*/
|
||||
offset: makeNumberProp(20),
|
||||
|
||||
/**
|
||||
* 动画持续时间(毫秒)
|
||||
* 类型:number
|
||||
* 默认值:300
|
||||
*/
|
||||
duration: makeNumberProp(300),
|
||||
|
||||
/**
|
||||
* 高亮区域的圆角大小
|
||||
* 类型:number
|
||||
* 默认值:8
|
||||
*/
|
||||
borderRadius: makeNumberProp(8),
|
||||
|
||||
/**
|
||||
* 高亮区域的内边距
|
||||
* 类型:number
|
||||
* 默认值:8
|
||||
*/
|
||||
padding: makeNumberProp(8),
|
||||
|
||||
/**
|
||||
* 上一步按钮文字
|
||||
*/
|
||||
prevText: makeStringProp('上一步'),
|
||||
|
||||
/**
|
||||
* 下一步按钮文字
|
||||
*/
|
||||
nextText: makeStringProp('下一步'),
|
||||
|
||||
/**
|
||||
* 跳过按钮文字
|
||||
*/
|
||||
skipText: makeStringProp('跳过'),
|
||||
|
||||
/**
|
||||
* 完成按钮文字
|
||||
*/
|
||||
finishText: makeStringProp('完成'),
|
||||
|
||||
/**
|
||||
* 安全偏移量,用于滚动计算时确保元素周围有足够的空间
|
||||
* 类型:number
|
||||
* 默认值:100
|
||||
*/
|
||||
bottomSafetyOffset: makeNumberProp(100),
|
||||
|
||||
/**
|
||||
* 顶部安全偏移量,用于滚动计算时确保元素周围有足够的空间
|
||||
* 类型:number
|
||||
* 默认值:0
|
||||
*/
|
||||
topSafetyOffset: makeNumberProp(0),
|
||||
|
||||
/**
|
||||
* 是否自定义顶部导航栏
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
customNav: makeBooleanProp(false),
|
||||
|
||||
/**
|
||||
* 点击蒙版是否可以下一步
|
||||
* 类型:boolean
|
||||
* 默认值:false
|
||||
*/
|
||||
clickMaskNext: makeBooleanProp(false),
|
||||
|
||||
/**
|
||||
* 高亮区域样式
|
||||
* 类型:object
|
||||
* 默认值:{}
|
||||
*/
|
||||
highlightStyle: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
|
||||
/**
|
||||
* 引导框的层级
|
||||
* 类型:number
|
||||
* 默认值:999998
|
||||
*/
|
||||
zIndex: makeNumberProp(999998),
|
||||
|
||||
/**
|
||||
* 是否显示引导按钮
|
||||
* 类型:boolean
|
||||
* 默认值:true
|
||||
*/
|
||||
showTourButtons: makeBooleanProp(true)
|
||||
}
|
||||
|
||||
export type TourProps = typeof tourProps
|
||||
472
src/uni_modules/wot-design-uni/components/wd-tour/wd-tour.vue
Normal file
472
src/uni_modules/wot-design-uni/components/wd-tour/wd-tour.vue
Normal file
@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<view class="wd-tour" v-if="modelValue" :style="{ zIndex: zIndex }" @touchmove.stop.prevent="noop">
|
||||
<view class="wd-tour__mask" @click.stop="handleMask">
|
||||
<slot name="highlight" :elementInfo="highlightElementInfo">
|
||||
<view class="wd-tour__highlight" :style="highlightStyle"></view>
|
||||
</slot>
|
||||
<view class="wd-tour__popover" :style="popoverStyle">
|
||||
<slot name="content">
|
||||
<view class="wd-tour__info">
|
||||
<rich-text :nodes="currentStep.content"></rich-text>
|
||||
</view>
|
||||
</slot>
|
||||
|
||||
<view class="wd-tour__buttons" v-if="showTourButtons">
|
||||
<!-- 上一步按钮 -->
|
||||
<view class="wd-tour__prev" v-if="currentIndex > 0" @click.stop="handlePrev">
|
||||
<slot name="prev">
|
||||
<view class="wd-tour__prev__default">{{ prevText }}</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 跳过按钮 -->
|
||||
<view class="wd-tour__skip" @click.stop="handleSkip">
|
||||
<slot name="skip" v-if="$slots.skip"></slot>
|
||||
<view class="wd-tour__skip__default" v-else>{{ skipText }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 下一步按钮 -->
|
||||
<view class="wd-tour__next" v-if="currentIndex !== steps.length - 1" @click.stop="handleNext">
|
||||
<slot name="next">
|
||||
<view class="wd-tour__next__default">
|
||||
{{ `${nextText}(${currentIndex + 1}/${steps.length})` }}
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 完成按钮 -->
|
||||
<view class="wd-tour__finish" v-if="currentIndex === steps.length - 1" @click.stop="handleFinish">
|
||||
<slot name="finish">
|
||||
<view class="wd-tour__finish__default">{{ finishText }}</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'wd-tour',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { tourProps } from './types'
|
||||
// #ifdef H5
|
||||
import useLockScroll from '../composables/useLockScroll'
|
||||
// #endif
|
||||
|
||||
interface ElementRect {
|
||||
top: number
|
||||
left: number
|
||||
width: number
|
||||
height: number
|
||||
bottom?: number
|
||||
right?: number
|
||||
}
|
||||
const props = defineProps(tourProps)
|
||||
const emit = defineEmits(['update:modelValue', 'update:current', 'change', 'prev', 'next', 'finish', 'skip', 'error'])
|
||||
// #ifdef H5
|
||||
const { lock, unlock } = useLockScroll(() => props.modelValue)
|
||||
// #endif
|
||||
|
||||
// 响应式数据
|
||||
const currentIndex = ref(0)
|
||||
const elementInfo = ref({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
const windowHeight = ref(0)
|
||||
const windowTop = ref(0)
|
||||
const isUp = ref(1) // 判断元素位置,确定提示信息在该元素的上方还是下方
|
||||
const oldscrollTop = ref(0) //记录上一次滚动位置
|
||||
const statusBarHeight = ref(0)
|
||||
const menuButtonInfo = ref(null as UniNamespace.GetMenuButtonBoundingClientRectRes | null)
|
||||
const topOffset = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const currentStep = computed(() => {
|
||||
return props.steps[currentIndex.value] || {}
|
||||
})
|
||||
// 提取公共的默认样式函数
|
||||
function getDefaultStyle() {
|
||||
return {
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
width: '100vw',
|
||||
height: '0',
|
||||
transition: props.duration + 'ms all',
|
||||
borderRadius: '0px',
|
||||
padding: '0px'
|
||||
}
|
||||
}
|
||||
// 提取公共的高亮样式计算函数
|
||||
function calculateHighlightStyle(padding: number, boxShadow: string) {
|
||||
return {
|
||||
transition: props.duration + 'ms all,boxShadow 0s,height 0s,width 0s',
|
||||
borderRadius: props.borderRadius + 'px',
|
||||
padding: padding + 'px',
|
||||
boxShadow: boxShadow
|
||||
}
|
||||
}
|
||||
const highlightStyle = computed(() => {
|
||||
// 如果元素信息尚未获取到,返回空样式避免闪烁
|
||||
if (!elementInfo.value.width && !elementInfo.value.height) {
|
||||
return getDefaultStyle()
|
||||
}
|
||||
const padding = props.padding
|
||||
// 根据是否显示蒙版来设置阴影效果
|
||||
const boxShadow = props.mask ? `0 0 0 100vh ${props.maskColor}` : 'none'
|
||||
|
||||
const baseStyle = calculateHighlightStyle(padding, boxShadow)
|
||||
return {
|
||||
...baseStyle,
|
||||
top: elementInfo.value.top - padding + 'px',
|
||||
left: elementInfo.value.left - padding + 'px',
|
||||
height: elementInfo.value.height + 'px',
|
||||
width: elementInfo.value.width + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
const popoverStyle = computed(() => {
|
||||
const style: {
|
||||
transition: string
|
||||
position: string
|
||||
left: string
|
||||
transform: string
|
||||
maxWidth: string
|
||||
textAlign: string
|
||||
zIndex: number
|
||||
top?: string
|
||||
bottom?: string
|
||||
} = {
|
||||
transition: props.duration + 'ms all',
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
maxWidth: '686rpx',
|
||||
textAlign: 'center',
|
||||
zIndex: props.zIndex + 1
|
||||
}
|
||||
const padding = props.padding
|
||||
if (isUp.value === 1) {
|
||||
// 提示在元素下方
|
||||
style.top = elementInfo.value.top + elementInfo.value.height + padding + Number(props.offset) + 'px'
|
||||
} else {
|
||||
// 提示在元素上方
|
||||
style.bottom = windowHeight.value + windowTop.value - elementInfo.value.top + padding + Number(props.offset) + 'px'
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
const highlightElementInfo = computed(() => {
|
||||
const padding = props.padding
|
||||
const boxShadow = props.mask ? `0 0 0 100vh ${props.maskColor}` : 'none'
|
||||
// 如果元素信息尚未获取到,返回空样式避免闪烁
|
||||
if (!elementInfo.value.width && !elementInfo.value.height) {
|
||||
return getDefaultStyle()
|
||||
}
|
||||
|
||||
const baseStyle = calculateHighlightStyle(padding, boxShadow)
|
||||
return {
|
||||
...baseStyle,
|
||||
top: elementInfo.value.top - padding + 'px',
|
||||
left: elementInfo.value.left - padding + 'px',
|
||||
width: elementInfo.value.width + padding * 2 + 'px', // 加上左右padding
|
||||
height: elementInfo.value.height + padding * 2 + 'px' // 加上上下padding
|
||||
}
|
||||
})
|
||||
function noop() {}
|
||||
// 方法
|
||||
function updateElementInfo() {
|
||||
// 每次更新元素信息时重新获取系统信息,确保准确性
|
||||
updateSystemInfo()
|
||||
|
||||
const element = currentStep.value.element
|
||||
if (!element) return
|
||||
try {
|
||||
const query = uni.createSelectorQuery()
|
||||
query
|
||||
.select(element)
|
||||
.boundingClientRect((res: any) => {
|
||||
if (!res) {
|
||||
console.error('无法找到元素:', element)
|
||||
emit('error', {
|
||||
message: '无法找到指定的引导元素',
|
||||
element: element
|
||||
})
|
||||
return
|
||||
}
|
||||
// 初始化元素信息
|
||||
initializeElementInfo(res)
|
||||
// 获取有效的页面边界
|
||||
const effectiveBoundaries = getEffectiveBoundaries()
|
||||
// 检查是否需要滚动
|
||||
const scrollNeeds = checkScrollNeeds(res, effectiveBoundaries)
|
||||
// 处理滚动逻辑
|
||||
handleScrolling(res, scrollNeeds, effectiveBoundaries)
|
||||
// 计算提示框显示位置
|
||||
calculateTipPosition(res, effectiveBoundaries)
|
||||
})
|
||||
.exec()
|
||||
} catch (error) {
|
||||
console.error('updateElementInfo error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新系统信息
|
||||
function updateSystemInfo() {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
windowHeight.value = sysInfo.windowHeight
|
||||
windowTop.value = sysInfo.windowTop || 0
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 0
|
||||
}
|
||||
|
||||
// 初始化元素信息
|
||||
function initializeElementInfo(res: ElementRect) {
|
||||
elementInfo.value = res
|
||||
// 调整元素位置信息,加上窗口顶部偏移量
|
||||
elementInfo.value.top = res.top + windowTop.value
|
||||
elementInfo.value.bottom = res.bottom + windowTop.value
|
||||
}
|
||||
// 获取有效的页面边界(顶部和底部安全区域)
|
||||
function getEffectiveBoundaries() {
|
||||
// 有效顶部边界初始化为窗口顶部 + 顶部偏移量
|
||||
let effectiveWindowTop = windowTop.value + Number(topOffset.value)
|
||||
// 有效底部边界为窗口高度
|
||||
let effectiveWindowBottom = windowHeight.value
|
||||
return {
|
||||
top: effectiveWindowTop,
|
||||
bottom: effectiveWindowBottom
|
||||
}
|
||||
}
|
||||
// 检查是否需要滚动
|
||||
function checkScrollNeeds(res: ElementRect, boundaries: { top: number; bottom: number }) {
|
||||
// 判断元素是否被顶部遮挡(需要向上滚动)
|
||||
const needScrollUp = res.top < boundaries.top
|
||||
// 判断元素是否被底部遮挡(需要向下滚动)
|
||||
const needScrollDown = (res.bottom !== undefined ? res.bottom : 0) + Number(props.bottomSafetyOffset) > boundaries.bottom
|
||||
return {
|
||||
up: needScrollUp, //提示框往上走
|
||||
down: needScrollDown //提示框往下走
|
||||
}
|
||||
}
|
||||
|
||||
// 处理滚动逻辑
|
||||
function handleScrolling(res: ElementRect, scrollNeeds, boundaries: { top: number; bottom: number }) {
|
||||
if (scrollNeeds.up) {
|
||||
// 元素被顶部遮挡,需要提示框往上走,页面往下走
|
||||
scrollUp(res, boundaries)
|
||||
} else if (scrollNeeds.down) {
|
||||
// 元素被底部遮挡,需要提示框向下走,页面向上走
|
||||
scrollDown(res)
|
||||
}
|
||||
}
|
||||
|
||||
// 向引导上滚动处理
|
||||
function scrollUp(res: ElementRect, boundaries: { top: number; bottom: number }) {
|
||||
// 计算需要滚动的距离
|
||||
let scrollDistance = oldscrollTop.value + res.top - props.padding - boundaries.top
|
||||
// 更新元素位置信息(滚动后)
|
||||
elementInfo.value.top = boundaries.top + props.padding
|
||||
elementInfo.value.bottom = windowHeight.value - (boundaries.top + props.padding)
|
||||
uni.pageScrollTo({
|
||||
scrollTop: scrollDistance,
|
||||
duration: Number(props.duration),
|
||||
success: () => {
|
||||
// 更新已滚动距离
|
||||
oldscrollTop.value = scrollDistance
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 引导向下滚动处理
|
||||
function scrollDown(res: ElementRect) {
|
||||
// 计算需要滚动的距离
|
||||
let scrollDistance = (res.bottom ?? 0) - windowHeight.value + props.padding + Number(props.bottomSafetyOffset)
|
||||
|
||||
// 更新元素位置信息(滚动后)
|
||||
elementInfo.value.top = windowHeight.value - res.height - props.padding - Number(props.bottomSafetyOffset) // 应该是减去安全偏移量
|
||||
elementInfo.value.bottom = windowHeight.value - props.padding - Number(props.bottomSafetyOffset)
|
||||
|
||||
uni.pageScrollTo({
|
||||
scrollTop: scrollDistance + oldscrollTop.value,
|
||||
duration: Number(props.duration),
|
||||
success: () => {
|
||||
// 更新已滚动距离
|
||||
oldscrollTop.value = scrollDistance + oldscrollTop.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算提示框显示位置(上方或下方)
|
||||
function calculateTipPosition(res: ElementRect, boundaries: { top: number; bottom: number }) {
|
||||
// 计算导航区域总高度
|
||||
let totalNavHeight = statusBarHeight.value
|
||||
|
||||
// #ifdef MP
|
||||
// 微信小程序平台考虑菜单按钮高度
|
||||
// if (props.customNav) {
|
||||
// // const menuBottom = this.menuButtonInfo.top + this.menuButtonInfo.height;
|
||||
// const menuBottom = menuButtonInfo.value.top;
|
||||
// totalNavHeight = Math.max(totalNavHeight, menuBottom);
|
||||
// }
|
||||
// #endif
|
||||
// totalNavHeight = Math.max(totalNavHeight, topOffset.value);
|
||||
|
||||
// 计算屏幕中心点位置
|
||||
const screenCenter = (windowHeight.value + totalNavHeight) / 2 + windowTop.value
|
||||
|
||||
// 计算元素中心点位置
|
||||
const elementCenter = res.top + res.height / 2 + windowTop.value
|
||||
|
||||
// 根据元素位置决定提示框显示在上方还是下方
|
||||
if (elementCenter < screenCenter) {
|
||||
isUp.value = 1 // 提示在元素下方
|
||||
} else {
|
||||
isUp.value = 0 // 提示在元素上方
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
if (currentIndex.value > 0) {
|
||||
const oldIndex = currentIndex.value
|
||||
currentIndex.value--
|
||||
emit('prev', {
|
||||
oldCurrent: oldIndex,
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length,
|
||||
isUp: isUp.value
|
||||
})
|
||||
emit('change', currentIndex.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (currentIndex.value < props.steps.length - 1) {
|
||||
const oldIndex = currentIndex.value
|
||||
currentIndex.value++
|
||||
emit('next', {
|
||||
oldCurrent: oldIndex,
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length,
|
||||
isUp: isUp.value
|
||||
})
|
||||
emit('change', currentIndex.value)
|
||||
} else {
|
||||
handleFinish()
|
||||
}
|
||||
}
|
||||
|
||||
function handleFinish() {
|
||||
emit('finish', {
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length
|
||||
})
|
||||
currentIndex.value = 0
|
||||
oldscrollTop.value = 0 // 重置滚动位置
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function handleSkip() {
|
||||
emit('skip', {
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length
|
||||
})
|
||||
currentIndex.value = 0
|
||||
oldscrollTop.value = 0 // 重置滚动位置
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
function handleMask() {
|
||||
if (props.clickMaskNext) {
|
||||
handleNext()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.current,
|
||||
(newVal) => {
|
||||
currentIndex.value = newVal
|
||||
}
|
||||
)
|
||||
// 监听 currentIndex 变化,同步到父组件
|
||||
watch(
|
||||
() => currentIndex.value,
|
||||
(newVal) => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
updateElementInfo()
|
||||
}, 50)
|
||||
})
|
||||
emit('update:current', newVal)
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 modelValue 变化,当组件显示时更新系统信息
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// 组件显示时重置滚动位置并更新系统信息
|
||||
oldscrollTop.value = 0
|
||||
updateSystemInfo()
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
updateElementInfo()
|
||||
emit('update:current', currentIndex.value)
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 初始化
|
||||
// updateSystemInfo()
|
||||
|
||||
// 所有平台统一处理逻辑
|
||||
if (props.customNav) {
|
||||
// 开启了自定义导航栏
|
||||
if (props.topSafetyOffset && Number(props.topSafetyOffset) > 0) {
|
||||
// 用户传入了顶部安全偏移量,优先使用用户设置的值
|
||||
topOffset.value = Number(props.topSafetyOffset)
|
||||
} else {
|
||||
// 未传入顶部偏移量
|
||||
// #ifdef MP
|
||||
// 微信小程序平台获取菜单按钮信息并使用其顶部位置
|
||||
menuButtonInfo.value = uni.getMenuButtonBoundingClientRect() || null
|
||||
topOffset.value = menuButtonInfo.value ? menuButtonInfo.value.top : 0
|
||||
// #endif
|
||||
// #ifndef MP
|
||||
// 非微信小程序平台默认为0
|
||||
topOffset.value = 0
|
||||
// #endif
|
||||
}
|
||||
} else {
|
||||
// 未开启自定义导航栏,直接使用用户传入的顶部安全偏移量
|
||||
topOffset.value = Number(props.topSafetyOffset) || 0
|
||||
}
|
||||
defineExpose({
|
||||
handlePrev,
|
||||
handleNext,
|
||||
handleFinish,
|
||||
handleSkip
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './index.scss';
|
||||
</style>
|
||||
1
src/uni_modules/wot-design-uni/global.d.ts
vendored
1
src/uni_modules/wot-design-uni/global.d.ts
vendored
@ -95,6 +95,7 @@ declare module 'vue' {
|
||||
WdFloatingPanel: typeof import('./components/wd-floating-panel/wd-floating-panel.vue')['default']
|
||||
WdSignature: typeof import('./components/wd-signature/wd-signature.vue')['default']
|
||||
WdRootPortal: typeof import('./components/wd-root-portal/wd-root-portal.vue')['default']
|
||||
WdTour: typeof import('./components/wd-tour/wd-tour.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
531
tests/components/wd-tour.test.ts
Normal file
531
tests/components/wd-tour.test.ts
Normal file
@ -0,0 +1,531 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import WdTour from '@/uni_modules/wot-design-uni/components/wd-tour/wd-tour.vue'
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// 模拟 useLockScroll 函数
|
||||
vi.mock('@/uni_modules/wot-design-uni/components/composables/useLockScroll', () => ({
|
||||
default: vi.fn(() => ({
|
||||
lock: vi.fn(),
|
||||
unlock: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
// 模拟uni对象
|
||||
const mockUni = {
|
||||
createSelectorQuery: vi.fn(() => ({
|
||||
select: vi.fn(() => ({
|
||||
boundingClientRect: vi.fn((callback) => {
|
||||
// 模拟元素位置信息
|
||||
setTimeout(() => {
|
||||
callback({
|
||||
top: 100,
|
||||
left: 50,
|
||||
width: 200,
|
||||
height: 100
|
||||
})
|
||||
}, 10)
|
||||
return {
|
||||
exec: vi.fn()
|
||||
}
|
||||
})
|
||||
}))
|
||||
})),
|
||||
pageScrollTo: vi.fn(),
|
||||
getSystemInfoSync: vi.fn(() => ({
|
||||
windowHeight: 600,
|
||||
windowTop: 0,
|
||||
statusBarHeight: 20
|
||||
})),
|
||||
getMenuButtonBoundingClientRect: vi.fn()
|
||||
}
|
||||
|
||||
// 模拟全局uni对象
|
||||
Object.defineProperty(global, 'uni', {
|
||||
value: mockUni
|
||||
})
|
||||
|
||||
describe('WdTour', () => {
|
||||
const steps = [
|
||||
{
|
||||
element: '#step1',
|
||||
content: '这是第一步'
|
||||
},
|
||||
{
|
||||
element: '#step2',
|
||||
content: '这是第二步'
|
||||
}
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// 重置模拟函数
|
||||
mockUni.createSelectorQuery.mockClear()
|
||||
mockUni.createSelectorQuery.mockImplementation(() => ({
|
||||
select: vi.fn(() => ({
|
||||
boundingClientRect: vi.fn((callback) => {
|
||||
// 模拟元素位置信息
|
||||
setTimeout(() => {
|
||||
callback({
|
||||
top: 100,
|
||||
left: 50,
|
||||
width: 200,
|
||||
height: 100
|
||||
})
|
||||
}, 10)
|
||||
return {
|
||||
exec: vi.fn()
|
||||
}
|
||||
})
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
// 测试基本渲染
|
||||
test('基本渲染', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps
|
||||
}
|
||||
})
|
||||
|
||||
// 等待 DOM 更新
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.classes()).toContain('wd-tour')
|
||||
expect(wrapper.find('.wd-tour__mask').exists()).toBe(true)
|
||||
})
|
||||
|
||||
// 测试隐藏状态
|
||||
test('隐藏状态', () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: false,
|
||||
steps
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('.wd-tour').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 测试步骤内容渲染
|
||||
test('渲染步骤内容', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
// 等待 DOM 更新和元素查询完成
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(wrapper.find('.wd-tour__info').exists()).toBe(true)
|
||||
// 验证富文本内容是否正确渲染
|
||||
const richText = wrapper.find('.wd-tour__info rich-text')
|
||||
expect(richText.exists()).toBe(true)
|
||||
})
|
||||
|
||||
// 测试按钮显示
|
||||
test('显示导航按钮', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 第一步应该显示"跳过"和"下一步"按钮
|
||||
expect(wrapper.find('.wd-tour__skip').exists()).toBe(true)
|
||||
expect(wrapper.find('.wd-tour__next').exists()).toBe(true)
|
||||
expect(wrapper.find('.wd-tour__prev').exists()).toBe(false)
|
||||
expect(wrapper.find('.wd-tour__finish').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 测试最后一步按钮
|
||||
test('最后一步显示完成按钮', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
// 更新 current 属性
|
||||
await wrapper.setProps({ current: 1 })
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
// 最后一步应该显示"上一步"和"完成"按钮
|
||||
expect(wrapper.find('.wd-tour__prev').exists()).toBe(true)
|
||||
expect(wrapper.find('.wd-tour__finish').exists()).toBe(true)
|
||||
expect(wrapper.find('.wd-tour__next').exists()).toBe(false)
|
||||
expect(wrapper.find('.wd-tour__skip').exists()).toBe(true)
|
||||
})
|
||||
// ... existing code ...
|
||||
|
||||
// 测试自定义按钮文字
|
||||
test('自定义按钮文字', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0,
|
||||
prevText: '上一页',
|
||||
nextText: '下一页',
|
||||
skipText: '忽略',
|
||||
finishText: '结束'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(wrapper.find('.wd-tour__skip__default').text()).toBe('忽略')
|
||||
expect(wrapper.find('.wd-tour__finish__default').exists()).toBe(false) // 不在第一页
|
||||
})
|
||||
|
||||
// 测试进度显示
|
||||
test('显示进度', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(wrapper.find('.wd-tour__next__default').text()).toContain('(1/2)')
|
||||
})
|
||||
|
||||
// 测试点击下一步
|
||||
test('点击下一步按钮', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const nextButton = wrapper.find('.wd-tour__next')
|
||||
await nextButton.trigger('click')
|
||||
|
||||
// 验证是否发出 next 事件
|
||||
expect(wrapper.emitted('next')).toBeTruthy()
|
||||
expect(wrapper.emitted('change')).toBeTruthy()
|
||||
})
|
||||
|
||||
// 测试点击上一步按钮
|
||||
test('点击上一步按钮', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
// 更新 current 属性
|
||||
await wrapper.setProps({ current: 1 })
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
const prevButton = wrapper.find('.wd-tour__prev')
|
||||
expect(prevButton.exists()).toBe(true)
|
||||
await prevButton.trigger('click')
|
||||
|
||||
// 验证是否发出 prev 事件
|
||||
expect(wrapper.emitted('prev')).toBeTruthy()
|
||||
expect(wrapper.emitted('change')).toBeTruthy()
|
||||
})
|
||||
|
||||
// 测试点击完成按钮
|
||||
test('点击完成按钮', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
// 更新 current 属性
|
||||
await wrapper.setProps({ current: 1 })
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
|
||||
const finishButton = wrapper.find('.wd-tour__finish')
|
||||
expect(finishButton.exists()).toBe(true)
|
||||
await finishButton.trigger('click')
|
||||
|
||||
// 验证是否发出 finish 事件和更新 modelValue
|
||||
expect(wrapper.emitted('finish')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
})
|
||||
|
||||
// 测试点击跳过按钮
|
||||
test('点击跳过按钮', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const skipButton = wrapper.find('.wd-tour__skip')
|
||||
await skipButton.trigger('click')
|
||||
|
||||
// 验证是否发出 skip 事件和更新 modelValue
|
||||
expect(wrapper.emitted('skip')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
})
|
||||
|
||||
// 测试点击蒙版
|
||||
test('点击蒙版触发下一步', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0,
|
||||
clickMaskNext: true
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const mask = wrapper.find('.wd-tour__mask')
|
||||
await mask.trigger('click')
|
||||
|
||||
// 验证是否触发下一步
|
||||
expect(wrapper.emitted('next')).toBeTruthy()
|
||||
})
|
||||
|
||||
// 测试禁用点击蒙版
|
||||
test('禁用点击蒙版', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0,
|
||||
clickMaskNext: false
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const mask = wrapper.find('.wd-tour__mask')
|
||||
await mask.trigger('click')
|
||||
|
||||
// 验证不触发下一步
|
||||
expect(wrapper.emitted('next')).toBeFalsy()
|
||||
})
|
||||
|
||||
// 测试蒙版显示
|
||||
test('显示/隐藏蒙版', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0,
|
||||
mask: false
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 等待元素信息更新完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 当 mask 为 false 时,boxShadow 应该为 none
|
||||
const highlightStyle = wrapper.find('.wd-tour__highlight').attributes('style')
|
||||
expect(highlightStyle).toContain('box-shadow: none')
|
||||
})
|
||||
|
||||
// 测试自定义高亮区域插槽
|
||||
test('使用自定义高亮区域插槽', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
},
|
||||
slots: {
|
||||
highlight: '<div class="custom-highlight">自定义高亮区域</div>'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(wrapper.find('.custom-highlight').exists()).toBe(true)
|
||||
expect(wrapper.find('.wd-tour__highlight').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 测试自定义内容插槽
|
||||
test('使用自定义内容插槽', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
},
|
||||
slots: {
|
||||
content: '<div class="custom-content">自定义内容</div>'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(wrapper.find('.custom-content').exists()).toBe(true)
|
||||
expect(wrapper.find('.wd-tour__info').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 测试自定义按钮插槽
|
||||
test('使用自定义按钮插槽', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
},
|
||||
slots: {
|
||||
prev: '<button class="custom-prev">上一步</button>',
|
||||
next: '<button class="custom-next">下一步</button>',
|
||||
skip: '<button class="custom-skip">跳过</button>',
|
||||
finish: '<button class="custom-finish">完成</button>'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 在第一页时,prev和finish按钮不应该存在
|
||||
expect(wrapper.find('.custom-prev').exists()).toBe(false)
|
||||
expect(wrapper.find('.custom-next').exists()).toBe(true)
|
||||
expect(wrapper.find('.custom-skip').exists()).toBe(true)
|
||||
expect(wrapper.find('.custom-finish').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 测试样式属性
|
||||
test('应用样式属性', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0,
|
||||
maskColor: 'rgba(255, 0, 0, 0.5)',
|
||||
offset: 30,
|
||||
borderRadius: 10,
|
||||
padding: 15
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 等待元素信息更新完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const highlightStyle = wrapper.find('.wd-tour__highlight').attributes('style')
|
||||
expect(highlightStyle).toContain('border-radius: 10px')
|
||||
expect(highlightStyle).toContain('padding: 15px')
|
||||
})
|
||||
|
||||
// 测试 zIndex 属性
|
||||
test('应用 zIndex', async () => {
|
||||
const zIndex = 10000
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0,
|
||||
zIndex
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const style = wrapper.find('.wd-tour').attributes('style')
|
||||
expect(style).toContain(`z-index: ${zIndex}`)
|
||||
})
|
||||
|
||||
// 测试禁用引导按钮
|
||||
test('禁用引导按钮', async () => {
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0,
|
||||
showTourButtons: false
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(wrapper.find('.wd-tour__buttons').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// 测试错误处理
|
||||
test('处理元素查找错误', async () => {
|
||||
// 模拟找不到元素的情况
|
||||
mockUni.createSelectorQuery.mockImplementation(() => ({
|
||||
select: vi.fn(() => ({
|
||||
boundingClientRect: vi.fn((callback) => {
|
||||
// 模拟找不到元素
|
||||
setTimeout(() => {
|
||||
callback(null)
|
||||
}, 10)
|
||||
return {
|
||||
exec: vi.fn()
|
||||
}
|
||||
})
|
||||
}))
|
||||
}))
|
||||
|
||||
const wrapper = mount(WdTour, {
|
||||
props: {
|
||||
modelValue: true,
|
||||
steps,
|
||||
current: 0
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// 验证是否发出 error 事件
|
||||
expect(wrapper.emitted('error')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user