tour-guide-coach 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/dist/TourOverlay.vue.d.ts +2 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +397 -0
- package/dist/store.d.ts +21 -0
- package/dist/style.css +1 -0
- package/package.json +48 -0
- package/src/TourOverlay.vue +239 -0
- package/src/configs/demo-form.ts +82 -0
- package/src/index.ts +14 -0
- package/src/store.ts +270 -0
- package/src/types.ts +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xzdszk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# vue-tour-guide
|
|
2
|
+
|
|
3
|
+
轻量级交互式漫游引导引擎,基于 **Vue 3**,**零外部依赖**。
|
|
4
|
+
|
|
5
|
+
样式沿用 Element Plus 设计语言(色彩、圆角、字号),但不需要安装任何组件库。如果项目已有 Element Plus,CSS 变量自动 fallback 到 `--el-*` 保持一致;独立使用时走内置默认值。
|
|
6
|
+
|
|
7
|
+
与 driver.js / intro.js 等传统引导库的核心区别:**引导跟随用户真实操作推进**,而非"下一步"按钮翻页。用户必须点击高亮按钮、输入必填字段、选择下拉项后,引导才会进入下一步。
|
|
8
|
+
|
|
9
|
+
## 特性
|
|
10
|
+
|
|
11
|
+
- **蒙版 + 元素高亮**:4 块遮罩围绕目标区域,中间可交互(点击/输入/选择)
|
|
12
|
+
- **事件驱动推步**:`trigger: 'click'` 用户必须点目标元素才推进,不给"下一步"按钮
|
|
13
|
+
- **表单门控**:`gate(resolve)` 轮询检测输入值/选中状态,未满足条件时"下一步"锁住
|
|
14
|
+
- **动态高亮扩展**:`expandSelector` 自动把 teleport 到 body 的 el-select 下拉面板、el-date-picker 日历面板纳入可交互区域
|
|
15
|
+
- **零业务代码侵入**:纯 CSS selector 定位现有 DOM,不需要给业务组件加 `data-tour` 属性或 `defineExpose`
|
|
16
|
+
- **等待目标 + 超时跳过**:目标元素不存在时用 MutationObserver 轮询等待,超时显示"跳过此步"而非自动跳(避免级联跳过)
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install tour-guide-coach
|
|
22
|
+
# 或从 GitHub
|
|
23
|
+
npm install github:2729305948/vue-tour-guide
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Peer dependencies:`vue@^3.4`。**零外部依赖,不需要 UI 组件库,不需要 Pinia。**
|
|
27
|
+
|
|
28
|
+
## 快速开始
|
|
29
|
+
|
|
30
|
+
### 1. 挂载全局蒙版组件
|
|
31
|
+
|
|
32
|
+
在应用 layout 根节点(或 `App.vue`)挂一次 `<TourOverlay />`,内部 `Teleport to="body"`,只在引导激活时渲染:
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<script setup>
|
|
36
|
+
import { TourOverlay } from 'vue-tour-guide'
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<el-container>
|
|
41
|
+
<el-main>...</el-main>
|
|
42
|
+
<TourOverlay />
|
|
43
|
+
</el-container>
|
|
44
|
+
</template>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 定义引导配置
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// tours/create-project.ts
|
|
51
|
+
import type { TourConfig } from 'vue-tour-guide'
|
|
52
|
+
|
|
53
|
+
export const tourCreateProject: TourConfig = {
|
|
54
|
+
key: 'create-project',
|
|
55
|
+
name: '新建项目引导',
|
|
56
|
+
accent: '#409eff',
|
|
57
|
+
steps: [
|
|
58
|
+
{
|
|
59
|
+
id: 'open-btn',
|
|
60
|
+
target: '#create-btn', // CSS selector
|
|
61
|
+
placement: 'bottom',
|
|
62
|
+
title: '点击「新建」',
|
|
63
|
+
content: '点击这个按钮打开表单弹窗。',
|
|
64
|
+
trigger: 'click',
|
|
65
|
+
afterClickWait: '.el-dialog__body .el-form-item:nth-child(1)',
|
|
66
|
+
clickDelay: 500
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'name-field',
|
|
70
|
+
target: '.el-dialog__body .el-form-item:nth-child(1) .el-input',
|
|
71
|
+
placement: 'right',
|
|
72
|
+
title: '填写名称',
|
|
73
|
+
content: '输入名称(必填)。',
|
|
74
|
+
trigger: 'input',
|
|
75
|
+
gate: (resolve) => {
|
|
76
|
+
const el = resolve('.el-dialog__body .el-form-item:nth-child(1) input')
|
|
77
|
+
return !!(el as HTMLInputElement | null)?.value?.trim()
|
|
78
|
+
},
|
|
79
|
+
gateHint: '请先输入名称'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'submit-btn',
|
|
83
|
+
target: '.el-dialog__footer .el-button--primary',
|
|
84
|
+
placement: 'top',
|
|
85
|
+
title: '提交',
|
|
86
|
+
content: '点击「确定」完成引导。',
|
|
87
|
+
trigger: 'click',
|
|
88
|
+
clickDelay: 500
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3. 触发引导
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { useTourStore } from 'vue-tour-guide'
|
|
98
|
+
import { tourCreateProject } from './tours/create-project'
|
|
99
|
+
|
|
100
|
+
const tour = useTourStore()
|
|
101
|
+
tour.startTour(tourCreateProject, undefined, () => {
|
|
102
|
+
console.log('引导完成')
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4. 运行 Demo
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cd demo
|
|
110
|
+
npm install
|
|
111
|
+
npm run dev
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
浏览器打开后自动启动引导,展示完整流程:点击按钮 → 弹窗 → 填名称 → 选分类(动态高亮下拉)→ 选日期(动态高亮日历)→ 提交。
|
|
115
|
+
|
|
116
|
+
## 引导规则
|
|
117
|
+
|
|
118
|
+
### Step 结构
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
interface TourStep {
|
|
122
|
+
id: string // 步骤唯一 ID
|
|
123
|
+
target: string // 高亮目标 CSS selector
|
|
124
|
+
expandSelector?: string // 动态扩展区域(teleport 弹层)
|
|
125
|
+
placement?: 'top'|'bottom'|'left'|'right'|'center'
|
|
126
|
+
title: string
|
|
127
|
+
content: string
|
|
128
|
+
trigger: StepTrigger // 推进方式
|
|
129
|
+
gate?: (resolve) => boolean // 门控条件
|
|
130
|
+
gateHint?: string // 门控未通过时的提示
|
|
131
|
+
waitForElement?: string // trigger=wait-dom 时等待的元素
|
|
132
|
+
autoDelay?: number // trigger=auto 时延时
|
|
133
|
+
clickDelay?: number // trigger=click 时点击后延时
|
|
134
|
+
afterClickWait?: string // 点击后等待的元素(如弹窗出现)
|
|
135
|
+
tip?: string // 绿色提示块
|
|
136
|
+
warn?: string // 橙色警示块
|
|
137
|
+
accent?: string // 覆盖全局强调色
|
|
138
|
+
beforeEnter?: () => void | Promise<void> // 进入前钩子(如路由跳转)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Trigger 类型语义
|
|
143
|
+
|
|
144
|
+
| trigger | 推进方式 | UI 表现 |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `click` | 用户点击高亮元素 | **不显示"下一步"按钮**,只显示"👆 请点击高亮区域"呼吸提示 |
|
|
147
|
+
| `input` | 输入框满足 gate 后手动点下一步 | gate 未通过时"下一步"锁住显示 gateHint |
|
|
148
|
+
| `change` | 值变化后(配合 gate) | 同上 |
|
|
149
|
+
| `manual` | 用户手动点"下一步" | 显示"跳过此步" + "下一步" |
|
|
150
|
+
| `wait-dom` | 等待 `waitForElement` 出现 | 居中 loading 提示 |
|
|
151
|
+
| `auto` | 延时自动推进 | 无 UI,静默跳 |
|
|
152
|
+
|
|
153
|
+
### Gate 门控
|
|
154
|
+
|
|
155
|
+
`gate(resolve)` 每 150ms 轮询一次,返回 `true` 才解锁"下一步"按钮。`resolve` 参数是元素解析器(宿主页面可传入自定义 resolver,默认为 `document.querySelector`)。
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
gate: (resolve) => {
|
|
159
|
+
const input = resolve('.my-form input[name="title"]')
|
|
160
|
+
return !!(input as HTMLInputElement)?.value?.trim()
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 动态高亮(expandSelector)
|
|
165
|
+
|
|
166
|
+
**核心场景**:Element Plus 的 `el-select`、`el-date-picker`、`el-cascader` 等组件的下拉面板/日历面板通过 `Teleport` 渲染到 `<body>` 末尾,不在原组件 DOM 树内。蒙版会挡住这些弹层,导致用户无法选择。
|
|
167
|
+
|
|
168
|
+
**解决方案**:给 step 加 `expandSelector`,引擎检测到该选择器匹配的元素可见时,自动把高亮区域扩展为 **target + expand 元素的联合矩形**。
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
{
|
|
172
|
+
id: 'category-select',
|
|
173
|
+
target: '.el-dialog__body .el-form-item:nth-child(2) .el-select',
|
|
174
|
+
expandSelector: '.el-select-dropdown', // ← el-select 展开的下拉面板
|
|
175
|
+
...
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**常用 expandSelector 值**:
|
|
180
|
+
|
|
181
|
+
| 组件 | 弹层选择器 |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `el-select` | `.el-select-dropdown` |
|
|
184
|
+
| `el-date-picker` / `el-time-picker` | `.el-picker-panel` |
|
|
185
|
+
| `el-cascader` | `.el-cascader__dropdown` |
|
|
186
|
+
| `el-tree-select` | `.el-select-dropdown`(内部是 tree) |
|
|
187
|
+
| `el-popover` / `el-tooltip` | `.el-popper` |
|
|
188
|
+
|
|
189
|
+
**原理**:引擎用 `MutationObserver` 监听 `document.body` 的 `childList + subtree + attributes(style/class)`,弹层出现/消失瞬间触发 `_updateRect` 重算。多个弹层共存时(如同时有多个 select 打开过),取**第一个 `offsetHeight > 0` 的可见元素**。
|
|
190
|
+
|
|
191
|
+
## 踩过的坑
|
|
192
|
+
|
|
193
|
+
### 1. `.tour-root` 必须 `pointer-events: none`
|
|
194
|
+
|
|
195
|
+
**症状**:高亮区域看起来正确,但点击没反应,被"隐形"拦截。
|
|
196
|
+
|
|
197
|
+
**根因**:根容器 `.tour-root` 是 `position: fixed; inset: 0` 的全屏 div,默认 `pointer-events: auto` 会拦截所有点击(包括高亮区域)。
|
|
198
|
+
|
|
199
|
+
**修复**:根容器 `pointer-events: none`,只让 `.tour-mask` 4 块遮罩、`.tour-popover` 气泡、`.tour-waiting` 等待框单独 `pointer-events: auto`。
|
|
200
|
+
|
|
201
|
+
### 2. `el-dialog` 选择器别用 `:has()` 复杂前缀
|
|
202
|
+
|
|
203
|
+
**症状**:`.el-dialog:has(.el-dialog__title) [data-tour="xxx"]` 找不到元素。
|
|
204
|
+
|
|
205
|
+
**根因**:`querySelector` 对 `:has()` 兼容性差(Safari 旧版不支持),且同一时刻只有一个 dialog,前缀完全多余。
|
|
206
|
+
|
|
207
|
+
**修复**:直接用 `[data-tour="xxx"]` 或 `.el-dialog__body .el-form-item:nth-child(N)`。
|
|
208
|
+
|
|
209
|
+
### 3. `el-input` 的 `data-tour` 落在根 div 不是 `<input>`
|
|
210
|
+
|
|
211
|
+
**症状**:`gate` 里 `el.value` 永远是 `undefined`。
|
|
212
|
+
|
|
213
|
+
**根因**:Vue 3 的 attribute fallthrough 把 `data-tour` 加到组件根元素 `<div class="el-input">` 上,不是内部的 `<input>`。
|
|
214
|
+
|
|
215
|
+
**修复**:gate 里用 `resolve('.target input')` 或 `el.querySelector('input')` 取真正的 input 元素。
|
|
216
|
+
|
|
217
|
+
### 4. `trigger: 'click'` 步骤不能显示"下一步"按钮
|
|
218
|
+
|
|
219
|
+
**症状**:用户不点高亮按钮,直接点"下一步"跳过了交互。
|
|
220
|
+
|
|
221
|
+
**根因**:气泡默认渲染"下一步"按钮,与 `trigger: 'click'` 的语义冲突。
|
|
222
|
+
|
|
223
|
+
**修复**:`trigger === 'click'` 时只渲染"👆 请点击高亮区域"呼吸提示,不给按钮。
|
|
224
|
+
|
|
225
|
+
### 5. 超时不能自动跳步骤
|
|
226
|
+
|
|
227
|
+
**症状**:Step 1 找不到元素 → 8s 超时 → 自动 `next()` → Step 2 也找不到 → 级联跳过 → 引导"跑完"但用户什么都没做。
|
|
228
|
+
|
|
229
|
+
**修复**:超时后设 `waitTimedOut = true`,UI 显示"未找到目标元素" + 手动"跳过此步"按钮。超时时间放宽到 15s。
|
|
230
|
+
|
|
231
|
+
### 6. 首步不要设"点击 Tab"
|
|
232
|
+
|
|
233
|
+
**症状**:用户已经在目标 Tab 上,引导要求"点击 Tab"→ 用户不知道要点什么 → 卡住或超时。
|
|
234
|
+
|
|
235
|
+
**修复**:如果目标状态已经达成(如已在 tasks tab),首步直接从"点击新建按钮"开始。Tab id 用 Element Plus 自动生成的 `#tab-{name}` 定位。
|
|
236
|
+
|
|
237
|
+
### 7. `destroy-on-close` 的 el-dialog 每次重建 DOM
|
|
238
|
+
|
|
239
|
+
**症状**:弹窗关闭后 `MutationObserver` 观察的旧节点失效。
|
|
240
|
+
|
|
241
|
+
**修复**:`_startWaitForElement` 观察 `document.body` 的 `childList + subtree`,弹窗重新挂载时能捕获到新节点。
|
|
242
|
+
|
|
243
|
+
### 8. 多个 popper 共存时 `querySelector` 只返回第一个
|
|
244
|
+
|
|
245
|
+
**症状**:`expandSelector: '.el-select-dropdown'` 匹配到的是之前打开过、现在 `display:none` 的旧面板。
|
|
246
|
+
|
|
247
|
+
**修复**:用 `querySelectorAll` + 遍历找第一个 `offsetHeight > 0` 的可见元素。
|
|
248
|
+
|
|
249
|
+
### 9. 引导激活时路由跳转要留渲染时间
|
|
250
|
+
|
|
251
|
+
**症状**:`router.push('/t/home')` 后立即 `startTour()` → 页面还在异步加载数据,按钮未渲染 → 卡等待。
|
|
252
|
+
|
|
253
|
+
**修复**:`setTimeout(() => tour.startTour(config), 1200)` 给页面渲染 + 数据加载留时间;引擎的 MutationObserver 兜底等元素出现。
|
|
254
|
+
|
|
255
|
+
### 10. Element Plus tab id 命名规则
|
|
256
|
+
|
|
257
|
+
**症状**:`target: '.el-tabs__item'` 匹配到多个。
|
|
258
|
+
|
|
259
|
+
**修复**:用 `#tab-{name}` 精确定位(Element Plus 自动生成的 id)。
|
|
260
|
+
|
|
261
|
+
## API
|
|
262
|
+
|
|
263
|
+
### `useTourStore()`
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const tour = useTourStore()
|
|
267
|
+
tour.startTour(config, resolver?, onComplete?)
|
|
268
|
+
tour.abort()
|
|
269
|
+
tour.next()
|
|
270
|
+
tour.skip()
|
|
271
|
+
tour.complete()
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 响应式状态
|
|
275
|
+
|
|
276
|
+
| 字段 | 类型 | 说明 |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| `active` | `Ref<boolean>` | 是否有引导正在播放 |
|
|
279
|
+
| `stepIndex` | `Ref<number>` | 当前步索引 |
|
|
280
|
+
| `totalSteps` | `Ref<number>` | 总步数 |
|
|
281
|
+
| `highlightRect` | `ShallowRef<HighlightRect>` | 当前高亮矩形 |
|
|
282
|
+
| `gatePass` | `Ref<boolean>` | 当前步 gate 是否通过 |
|
|
283
|
+
| `waitingTarget` | `Ref<boolean>` | 是否等待目标元素 |
|
|
284
|
+
| `waitTimedOut` | `Ref<boolean>` | 等待是否超时 |
|
|
285
|
+
|
|
286
|
+
### `resolver` 参数(可选)
|
|
287
|
+
|
|
288
|
+
宿主页面可提供自定义元素解析器,把 `target` 字符串映射为 `HTMLElement`。默认走 `document.querySelector`。用于组件 ref 模式(不通过 CSS 定位)。
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const resolver: TargetResolver = (target) => {
|
|
292
|
+
const [refName, childSel] = target.split(':')
|
|
293
|
+
const el = (dialogRef.value as any)[refName]?.$el
|
|
294
|
+
return childSel ? el?.querySelector(childSel) : el
|
|
295
|
+
}
|
|
296
|
+
tour.startTour(config, resolver)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## 许可
|
|
300
|
+
|
|
301
|
+
MIT
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("vue"),K=150,J=12e3,l=t.shallowReactive({active:!1,config:null,stepIndex:0,totalSteps:0,highlightRect:null,targetEl:null,gatePass:!0,waitingTarget:!1,waitTimedOut:!1});let $=null,_=null,h=null,d=null,V=null,k=null,w=null,v=null,x=null,y=null;function O(){var e;return((e=l.config)==null?void 0:e.steps[l.stepIndex])??null}function Q(){return O()}function X(e,o,a){l.active&&b(),l.config=e,l.stepIndex=0,l.totalSteps=e.steps.length,l.gatePass=!0,$=a??null,_=o??null,l.active=!0,W()}function b(){R(),l.active=!1,l.config=null,l.highlightRect=null,l.targetEl=null,_=null,$=null}function L(){R(),l.active=!1,l.highlightRect=null,l.targetEl=null;const e=$;_=null,$=null,e==null||e()}function p(){R();const e=l.config;if(e){if(l.stepIndex>=e.steps.length-1){L();return}l.stepIndex+=1,W()}}function P(){p()}function W(){var a;const e=O();if(!e){L();return}y=e,l.gatePass=!e.gate,l.waitingTarget=!0,l.waitTimedOut=!1,l.highlightRect=null,l.targetEl=null;const o=(a=e.beforeEnter)==null?void 0:a.call(e);o instanceof Promise?o.then(()=>M(e)):requestAnimationFrame(()=>M(e))}function M(e){if(e.trigger==="auto"){l.waitingTarget=!1,et(e);return}if(e.trigger==="wait-dom"){F(e);return}const o=T(e.target);if(o){I(o,e);return}H(e.target,()=>{const a=T(e.target);I(a,e)})}function T(e){if(_){const o=_(e);if(o)return o}try{return document.querySelector(e)}catch{return null}}function I(e,o){if(!e){l.waitTimedOut=!0;return}l.waitingTarget=!1,l.waitTimedOut=!1,l.targetEl=e,N(e),Z(e,o),tt(o),Y(e)}function N(e){const o=e.getBoundingClientRect(),a=6;let i={top:o.top-a,left:o.left-a,width:o.width+a*2,height:o.height+a*2};const s=y==null?void 0:y.expandSelector;if(s){const m=document.querySelectorAll(s);let E=null;for(const u of m)if(u.offsetHeight>0){E=u;break}if(E){const u=E.getBoundingClientRect(),B=Math.min(i.top,u.top-a),f=Math.min(i.left,u.left-a),C=Math.max(i.left+i.width,u.right+a),z=Math.max(i.top+i.height,u.bottom+a);i={top:B,left:f,width:C-f,height:z-B}}}l.highlightRect=i}function Y(e){A(),v=()=>N(e),window.addEventListener("scroll",v,!0),window.addEventListener("resize",v),w=new ResizeObserver(()=>N(e)),w.observe(e),(y==null?void 0:y.expandSelector)&&(x=new MutationObserver(()=>N(e)),x.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style","class"]}))}function A(){v&&(window.removeEventListener("scroll",v,!0),window.removeEventListener("resize",v)),v=null,w==null||w.disconnect(),w=null,x==null||x.disconnect(),x=null}function Z(e,o){if(o.trigger==="click"){const a=()=>{e.removeEventListener("click",a);const i=o.clickDelay??350;o.afterClickWait?F({waitForElement:o.afterClickWait,clickDelay:i}):k=setTimeout(()=>p(),i)};e.addEventListener("click",a)}}function tt(e){if(!e.gate){l.gatePass=!0;return}const o=a=>T(a);l.gatePass=e.gate(o),!l.gatePass&&(h=setInterval(()=>{e.gate(o)&&(l.gatePass=!0,h&&(clearInterval(h),h=null))},K))}function F(e){const o=e.waitForElement;if(!o){p();return}if(T(o)){k=setTimeout(()=>p(),e.clickDelay??300);return}H(o,()=>{k=setTimeout(()=>p(),e.clickDelay??300)})}function H(e,o){let a=!1;const i=()=>{a||(a=!0,d==null||d.disconnect(),d=null,o())};d=new MutationObserver(()=>{T(e)&&i()}),d.observe(document.body,{childList:!0,subtree:!0}),V=setTimeout(i,J)}function et(e){k=setTimeout(()=>p(),e.autoDelay??600)}function R(){A(),h&&(clearInterval(h),h=null),d&&(d.disconnect(),d=null),V&&(clearTimeout(V),V=null),k&&(clearTimeout(k),k=null)}const nt={key:0,class:"tour-root"},ot={class:"tp-head"},lt={key:0,class:"tp-required"},it={class:"tp-title"},at={class:"tp-of"},rt={class:"tp-content"},ct={key:0,class:"tp-tip"},st={key:1,class:"tp-warn"},ut={class:"tp-foot"},dt={key:0,class:"tp-click-hint"},pt={key:1,class:"tp-btns"},mt={key:2,class:"tp-gate"},ft={key:2,class:"tour-waiting"},gt={key:0,class:"tg-spin",viewBox:"0 0 1024 1024",width:"28",height:"28",fill:"currentColor"},ht={key:1,viewBox:"0 0 1024 1024",width:"28",height:"28",fill:"#e6a23c"},vt=t.defineComponent({__name:"TourOverlay",setup(e){const o=l;function a(n){n.key==="Escape"&&o.active&&b()}t.watch(()=>o.active,n=>{n?window.addEventListener("keydown",a):window.removeEventListener("keydown",a)}),t.onBeforeUnmount(()=>window.removeEventListener("keydown",a));const i=t.computed(()=>Q()),s=t.computed(()=>o.highlightRect),m=t.computed(()=>{var n,r;return((n=i.value)==null?void 0:n.accent)??((r=o.config)==null?void 0:r.accent)??"#409eff"}),E=t.computed(()=>o.stepIndex>=o.totalSteps-1),u=t.computed(()=>{const n=i.value;return n?!n.gate&&n.trigger==="manual":!1});function B(){const n=s.value;return n?{top:{left:0,top:0,width:"100vw",height:`${Math.max(n.top,0)}px`},bottom:{left:0,top:`${n.top+n.height}px`,width:"100vw",height:`calc(100vh - ${n.top+n.height}px)`},left:{left:0,top:`${n.top}px`,width:`${Math.max(n.left,0)}px`,height:`${n.height}px`},right:{left:`${n.left+n.width}px`,top:`${n.top}px`,width:`calc(100vw - ${n.left+n.width}px)`,height:`${n.height}px`}}:null}const f=t.computed(B),C=t.computed(()=>{var n;return((n=f.value)==null?void 0:n.top)??{display:"none"}}),z=t.computed(()=>{var n;return((n=f.value)==null?void 0:n.bottom)??{display:"none"}}),q=t.computed(()=>{var n;return((n=f.value)==null?void 0:n.left)??{display:"none"}}),G=t.computed(()=>{var n;return((n=f.value)==null?void 0:n.right)??{display:"none"}}),U=t.computed(()=>{const n=s.value;return n?{position:"fixed",top:`${n.top}px`,left:`${n.left}px`,width:`${n.width}px`,height:`${n.height}px`,borderRadius:"8px",border:`2px solid ${m.value}`,boxShadow:`0 0 0 3px ${m.value}33, 0 0 20px ${m.value}44`,pointerEvents:"none",transition:"all 0.35s cubic-bezier(0.4,0,0.2,1)"}:{display:"none"}}),j=t.computed(()=>{var D;const n=s.value;if(!n)return{position:"fixed",top:"50%",left:"50%",transform:"translate(-50%,-50%)"};const r=((D=i.value)==null?void 0:D.placement)??"bottom",g=14,c={position:"fixed",zIndex:100002};switch(r){case"top":return{...c,left:`${n.left}px`,bottom:`calc(100vh - ${n.top-g}px)`,maxWidth:"420px"};case"bottom":return{...c,left:`${n.left}px`,top:`${n.top+n.height+g}px`,maxWidth:"420px"};case"left":return{...c,right:`calc(100vw - ${n.left-g}px)`,top:`${n.top}px`,maxWidth:"360px"};case"right":return{...c,left:`${n.left+n.width+g}px`,top:`${n.top}px`,maxWidth:"360px"};default:return{...c,top:"50%",left:"50%",transform:"translate(-50%,-50%)",maxWidth:"480px"}}});function S(){}return(n,r)=>{var g;return t.openBlock(),t.createBlock(t.Teleport,{to:"body"},[t.unref(o).active?(t.openBlock(),t.createElementBlock("div",nt,[t.createElementVNode("div",{class:"tour-mask",style:t.normalizeStyle(C.value),onClick:S},null,4),t.createElementVNode("div",{class:"tour-mask",style:t.normalizeStyle(z.value),onClick:S},null,4),t.createElementVNode("div",{class:"tour-mask",style:t.normalizeStyle(q.value),onClick:S},null,4),t.createElementVNode("div",{class:"tour-mask",style:t.normalizeStyle(G.value),onClick:S},null,4),s.value?(t.openBlock(),t.createElementBlock("div",{key:0,class:"tour-highlight",style:t.normalizeStyle(U.value)},null,4)):t.createCommentVNode("",!0),i.value&&!t.unref(o).waitingTarget?(t.openBlock(),t.createElementBlock("div",{key:1,class:"tour-popover",style:t.normalizeStyle(j.value)},[t.createElementVNode("div",ot,[t.createElementVNode("span",{class:"tp-step-no",style:t.normalizeStyle({background:m.value})},t.toDisplayString(t.unref(o).stepIndex+1),5),i.value.gate?(t.openBlock(),t.createElementBlock("span",lt,"*")):t.createCommentVNode("",!0),t.createElementVNode("span",it,t.toDisplayString(i.value.title),1),t.createElementVNode("span",at,"第 "+t.toDisplayString(t.unref(o).stepIndex+1)+" / "+t.toDisplayString(t.unref(o).totalSteps)+" 步",1)]),t.createElementVNode("p",rt,t.toDisplayString(i.value.content),1),i.value.tip?(t.openBlock(),t.createElementBlock("div",ct,"💡 "+t.toDisplayString(i.value.tip),1)):t.createCommentVNode("",!0),i.value.warn?(t.openBlock(),t.createElementBlock("div",st,"⚠️ "+t.toDisplayString(i.value.warn),1)):t.createCommentVNode("",!0),t.createElementVNode("div",ut,[t.createElementVNode("button",{class:"tg-btn tg-btn--link tg-btn--sm",onClick:r[0]||(r[0]=c=>t.unref(b)())},"退出引导"),i.value.trigger==="click"?(t.openBlock(),t.createElementBlock("span",dt," 👆 请点击高亮区域 ")):t.unref(o).gatePass?(t.openBlock(),t.createElementBlock("span",pt,[u.value?(t.openBlock(),t.createElementBlock("button",{key:0,class:"tg-btn tg-btn--sm",onClick:r[1]||(r[1]=c=>t.unref(P)())},"跳过此步")):t.createCommentVNode("",!0),t.createElementVNode("button",{class:"tg-btn tg-btn--primary tg-btn--sm",style:t.normalizeStyle({background:m.value,borderColor:m.value}),onClick:r[2]||(r[2]=c=>t.unref(p)())},t.toDisplayString(E.value?"完成":"下一步"),5)])):(t.openBlock(),t.createElementBlock("span",mt,[r[5]||(r[5]=t.createElementVNode("svg",{class:"tp-gate-icon",viewBox:"0 0 1024 1024",width:"14",height:"14",fill:"currentColor"},[t.createElementVNode("path",{d:"M512 160a96 96 0 0 1 96 96v64H416v-64a96 96 0 0 1 96-96zm0-64a160 160 0 0 0-160 160v64h-32a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h384a64 64 0 0 0 64-64V384a64 64 0 0 0-64-64h-32v-64A160 160 0 0 0 512 96zM320 448h384v320H320V448z"})],-1)),t.createTextVNode(" "+t.toDisplayString(i.value.gateHint||"请先完成本步操作"),1)]))])],4)):t.createCommentVNode("",!0),t.unref(o).waitingTarget?(t.openBlock(),t.createElementBlock("div",ft,[t.unref(o).waitTimedOut?(t.openBlock(),t.createElementBlock("svg",ht,[...r[7]||(r[7]=[t.createElementVNode("path",{d:"M512 64L64 896h896L512 64zm0 192l288 576H224l288-576zm-32 224v128h64V480h-64zm0 160v64h64v-64h-64z"},null,-1)])])):(t.openBlock(),t.createElementBlock("svg",gt,[...r[6]||(r[6]=[t.createElementVNode("path",{d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 64a384 384 0 1 0 0 768 384 384 0 0 0 0-768z",opacity:".25"},null,-1),t.createElementVNode("path",{d:"M512 64a448 448 0 0 1 448 448h-64A384 384 0 0 0 512 128V64z"},null,-1)])])),t.createElementVNode("p",null,t.toDisplayString(t.unref(o).waitTimedOut?"未找到目标元素,可能是页面状态不符":((g=i.value)==null?void 0:g.content)??"等待页面加载..."),1),t.unref(o).waitTimedOut?(t.openBlock(),t.createElementBlock("button",{key:2,class:"tg-btn tg-btn--warning tg-btn--sm",onClick:r[3]||(r[3]=c=>t.unref(p)())},"跳过此步")):t.createCommentVNode("",!0),t.createElementVNode("button",{class:"tg-btn tg-btn--sm",onClick:r[4]||(r[4]=c=>t.unref(b)())},"退出引导")])):t.createCommentVNode("",!0)])):t.createCommentVNode("",!0)])}}}),kt=(e,o)=>{const a=e.__vccOpts||e;for(const[i,s]of o)a[i]=s;return a},yt=kt(vt,[["__scopeId","data-v-9602901d"]]);exports.TourOverlay=yt;exports.abort=b;exports.complete=L;exports.next=p;exports.skip=P;exports.startTour=X;exports.tourState=l;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vue-tour-guide · 公共入口
|
|
3
|
+
* -----------------------------------------------------------------
|
|
4
|
+
* 零外部依赖(仅需 Vue 3),全局单例模式。
|
|
5
|
+
*
|
|
6
|
+
* 使用方式:
|
|
7
|
+
* import { tourState, startTour, abort, TourOverlay } from 'vue-tour-guide'
|
|
8
|
+
* // 在 layout 中挂 <TourOverlay />
|
|
9
|
+
* // 业务页调用 startTour(config)
|
|
10
|
+
*/
|
|
11
|
+
export { tourState, startTour, abort, next, skip, complete } from './store';
|
|
12
|
+
export type { TargetResolver } from './store';
|
|
13
|
+
export { default as TourOverlay } from './TourOverlay.vue';
|
|
14
|
+
export type { TourConfig, TourStep, StepTrigger, PopPlacement, HighlightRect } from './types';
|