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',
|
link: '/en-US/component/index-bar',
|
||||||
text: 'IndexBar'
|
text: 'IndexBar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: '/en-US/component/tour',
|
||||||
|
text: 'Tour'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -248,6 +248,9 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
link: '/component/index-bar',
|
link: '/component/index-bar',
|
||||||
text: 'IndexBar 索引栏'
|
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",
|
"ri-zhou-yue-qie-huan": "Day Week Month Switching",
|
||||||
"ring-lei-xing-loading": "Ring type loading",
|
"ring-lei-xing-loading": "Ring type loading",
|
||||||
"rootportal-title": "Root Portal",
|
"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": "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.",
|
"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",
|
"ruan-jian-gong-cheng": "software engineering",
|
||||||
|
|||||||
@ -1175,6 +1175,7 @@
|
|||||||
"top-right": "top right",
|
"top-right": "top right",
|
||||||
"tou-bi": "投币",
|
"tou-bi": "投币",
|
||||||
"tou-xiang-gu-jia-ping": "头像骨架屏",
|
"tou-xiang-gu-jia-ping": "头像骨架屏",
|
||||||
|
"tour-title": "Tour 漫游",
|
||||||
"transition-dong-hua": "Transition 动画",
|
"transition-dong-hua": "Transition 动画",
|
||||||
"transition-title": "Transition 动画",
|
"transition-title": "Transition 动画",
|
||||||
"tu-biao": "图标",
|
"tu-biao": "图标",
|
||||||
|
|||||||
@ -46,8 +46,8 @@
|
|||||||
/* SDK配置 */
|
/* SDK配置 */
|
||||||
"sdkConfigs" : {}
|
"sdkConfigs" : {}
|
||||||
},
|
},
|
||||||
"compatible": {
|
"compatible" : {
|
||||||
"ignoreVersion": true
|
"ignoreVersion" : true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/* 快应用特有相关 */
|
/* 快应用特有相关 */
|
||||||
|
|||||||
@ -1326,6 +1326,21 @@
|
|||||||
"navigationBarTitleText": "%rootportal-title%"
|
"navigationBarTitleText": "%rootportal-title%"
|
||||||
// #endif
|
// #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',
|
id: 'indexBar',
|
||||||
name: t('indexbar-suo-yin-lan')
|
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']
|
WdFloatingPanel: typeof import('./components/wd-floating-panel/wd-floating-panel.vue')['default']
|
||||||
WdSignature: typeof import('./components/wd-signature/wd-signature.vue')['default']
|
WdSignature: typeof import('./components/wd-signature/wd-signature.vue')['default']
|
||||||
WdRootPortal: typeof import('./components/wd-root-portal/wd-root-portal.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