From 4aaf257166337fff45d7f54d7159f4aad6e1c324 Mon Sep 17 00:00:00 2001
From: Xiabaiyou <66239072+Xiabaiyou@users.noreply.github.com>
Date: Mon, 10 Nov 2025 19:12:20 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20=E6=96=B0=E5=A2=9E=20tour?=
=?UTF-8?q?=20=E6=BC=AB=E6=B8=B8=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/.vitepress/locales/en-US.ts | 4 +
docs/.vitepress/locales/zh-CN.ts | 3 +
docs/component/tour.md | 254 +++++++++
src/locale/en-US.json | 1 +
src/locale/zh-CN.json | 1 +
src/manifest.json | 4 +-
src/pages.json | 15 +
src/pages/index/Index.vue | 4 +
src/subPages/tour/Index.vue | 446 +++++++++++++++
.../components/wd-tour/index.scss | 123 ++++
.../components/wd-tour/types.ts | 153 +++++
.../components/wd-tour/wd-tour.vue | 472 ++++++++++++++++
src/uni_modules/wot-design-uni/global.d.ts | 1 +
tests/components/wd-tour.test.ts | 531 ++++++++++++++++++
14 files changed, 2010 insertions(+), 2 deletions(-)
create mode 100644 docs/component/tour.md
create mode 100644 src/subPages/tour/Index.vue
create mode 100644 src/uni_modules/wot-design-uni/components/wd-tour/index.scss
create mode 100644 src/uni_modules/wot-design-uni/components/wd-tour/types.ts
create mode 100644 src/uni_modules/wot-design-uni/components/wd-tour/wd-tour.vue
create mode 100644 tests/components/wd-tour.test.ts
diff --git a/docs/.vitepress/locales/en-US.ts b/docs/.vitepress/locales/en-US.ts
index f16e3aed..ebb8e3c0 100644
--- a/docs/.vitepress/locales/en-US.ts
+++ b/docs/.vitepress/locales/en-US.ts
@@ -241,6 +241,10 @@ export default defineConfig({
{
link: '/en-US/component/index-bar',
text: 'IndexBar'
+ },
+ {
+ link: '/en-US/component/tour',
+ text: 'Tour'
}
]
},
diff --git a/docs/.vitepress/locales/zh-CN.ts b/docs/.vitepress/locales/zh-CN.ts
index 512827ef..8fcf5bfa 100644
--- a/docs/.vitepress/locales/zh-CN.ts
+++ b/docs/.vitepress/locales/zh-CN.ts
@@ -248,6 +248,9 @@ export default defineConfig({
{
link: '/component/index-bar',
text: 'IndexBar 索引栏'
+ },{
+ link:'/component/tour',
+ text: 'Tour 漫游'
}
]
},
diff --git a/docs/component/tour.md b/docs/component/tour.md
new file mode 100644
index 00000000..0ff96404
--- /dev/null
+++ b/docs/component/tour.md
@@ -0,0 +1,254 @@
+# Guide 漫游组件
+
+用于引导用户逐步了解应用功能的组件,可以高亮显示页面中的特定元素并提供说明。
+
+## 基本使用
+
+通过 `steps` 属性设置引导步骤,通过 `v-model` 控制显示隐藏。
+
+```html
+
+
+
+ 第一步
+ 这是引导的第一步,介绍基本功能
+
+
+
+ 第二步
+ 这是引导的第二步,展示更多功能
+
+
+
+
+
+
+
+```
+
+### 自定义引导内容
+
+通过 `content` 插槽可以自定义引导内容。
+
+```html
+
+
+
+
+ 自定义引导内容区域
+
+
+
+```
+
+### 自定义高亮区域
+
+通过 `highlight` 插槽可以自定义高亮区域样式。
+
+```html
+
+
+
+
+
+```
+
+### 自定义按钮
+
+通过 `prev`、`next`、`skip`、`finish` 插槽可以自定义按钮。
+
+```html
+
+
+ 下一步
+
+
+ 完成
+
+
+```
+
+### 点击蒙版继续
+
+通过 `click-mask-next` 属性可以设置点击蒙版是否可以下一步。
+
+```html
+
+```
+
+### 自定义蒙版样式
+
+通过 `mask-color`、`offset`、`border-radius`、`padding` 属性可以自定义蒙版样式。
+
+```html
+
+```
+
+### 关闭蒙版
+
+通过 `mask` 属性可以控制是否显示蒙版。
+
+```html
+
+```
+
+### 控制当前步骤
+
+通过 `v-model:current` 可以控制当前步骤。
+
+```html
+
+ 跳转到第三步
+
+
+```
+
+## 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` 或修改自定义高亮区域的样式,确保按钮可点击。
\ No newline at end of file
diff --git a/src/locale/en-US.json b/src/locale/en-US.json
index dafa0e62..d0c7a05d 100644
--- a/src/locale/en-US.json
+++ b/src/locale/en-US.json
@@ -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",
diff --git a/src/locale/zh-CN.json b/src/locale/zh-CN.json
index d0f37331..f8bb1541 100644
--- a/src/locale/zh-CN.json
+++ b/src/locale/zh-CN.json
@@ -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": "图标",
diff --git a/src/manifest.json b/src/manifest.json
index fb33615e..d779f695 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -46,8 +46,8 @@
/* SDK配置 */
"sdkConfigs" : {}
},
- "compatible": {
- "ignoreVersion": true
+ "compatible" : {
+ "ignoreVersion" : true
}
},
/* 快应用特有相关 */
diff --git a/src/pages.json b/src/pages.json
index 1a0ed7ef..dcaf6c7d 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -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
+ }
}
]
}
diff --git a/src/pages/index/Index.vue b/src/pages/index/Index.vue
index ba5149d2..edd68f16 100644
--- a/src/pages/index/Index.vue
+++ b/src/pages/index/Index.vue
@@ -143,6 +143,10 @@ const list = computed(() => [
{
id: 'indexBar',
name: t('indexbar-suo-yin-lan')
+ },
+ {
+ id: 'tour',
+ name: t('tour-title')
}
]
},
diff --git a/src/subPages/tour/Index.vue b/src/subPages/tour/Index.vue
new file mode 100644
index 00000000..b62d1dc0
--- /dev/null
+++ b/src/subPages/tour/Index.vue
@@ -0,0 +1,446 @@
+
+
+
+
+
+ 第一步
+ 这是引导的第一步,介绍基本功能
+
+
+
+
+
+ 第二步
+ 这是引导的第二步,展示更多功能
+
+
+
+
+
+ 第三步
+ 这是引导的第三步,深入功能介绍
+
+
+
+
+
+ 第四步
+ 这是引导的最后一步,总结功能
+
+
+
+
+
+
+
+ 开始引导
+
+
+
+
+
+
+ 点击蒙版继续
+
+
+
+
+
+
+ 自定义蒙版
+
+
+
+
+
+
+ 关闭蒙版
+
+
+
+
+
+
+ 自定义高亮
+
+
+
+
+
+
+ 自定义内容
+
+
+
+
+
+
+
+ 跳转到第三步开始引导
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 自定义引导内容区域
+
+
+
+
+ 下一步
+
+
+
+ 完成
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/uni_modules/wot-design-uni/components/wd-tour/index.scss b/src/uni_modules/wot-design-uni/components/wd-tour/index.scss
new file mode 100644
index 00000000..ff93004f
--- /dev/null
+++ b/src/uni_modules/wot-design-uni/components/wd-tour/index.scss
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/uni_modules/wot-design-uni/components/wd-tour/types.ts b/src/uni_modules/wot-design-uni/components/wd-tour/types.ts
new file mode 100644
index 00000000..a90009a0
--- /dev/null
+++ b/src/uni_modules/wot-design-uni/components/wd-tour/types.ts
@@ -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(),
+ /**
+ * 引导框的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>,
+ default: () => ({})
+ },
+
+ /**
+ * 引导框的层级
+ * 类型:number
+ * 默认值:999998
+ */
+ zIndex: makeNumberProp(999998),
+
+ /**
+ * 是否显示引导按钮
+ * 类型:boolean
+ * 默认值:true
+ */
+ showTourButtons: makeBooleanProp(true)
+}
+
+export type TourProps = typeof tourProps
diff --git a/src/uni_modules/wot-design-uni/components/wd-tour/wd-tour.vue b/src/uni_modules/wot-design-uni/components/wd-tour/wd-tour.vue
new file mode 100644
index 00000000..b4e845c6
--- /dev/null
+++ b/src/uni_modules/wot-design-uni/components/wd-tour/wd-tour.vue
@@ -0,0 +1,472 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ prevText }}
+
+
+
+
+
+
+ {{ skipText }}
+
+
+
+
+
+
+ {{ `${nextText}(${currentIndex + 1}/${steps.length})` }}
+
+
+
+
+
+
+
+ {{ finishText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/uni_modules/wot-design-uni/global.d.ts b/src/uni_modules/wot-design-uni/global.d.ts
index 94014a34..5ec7544e 100644
--- a/src/uni_modules/wot-design-uni/global.d.ts
+++ b/src/uni_modules/wot-design-uni/global.d.ts
@@ -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']
}
}
diff --git a/tests/components/wd-tour.test.ts b/tests/components/wd-tour.test.ts
new file mode 100644
index 00000000..028beca8
--- /dev/null
+++ b/tests/components/wd-tour.test.ts
@@ -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: '自定义高亮区域
'
+ }
+ })
+
+ 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: '自定义内容
'
+ }
+ })
+
+ 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: '',
+ next: '',
+ skip: '',
+ finish: ''
+ }
+ })
+
+ 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()
+ })
+})