这一版本优化了很多
This commit is contained in:
145
node_modules/wot-design-uni/components/wd-tour/index.scss
generated
vendored
Normal file
145
node_modules/wot-design-uni/components/wd-tour/index.scss
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
@import './../common/abstracts/_mixin.scss';
|
||||
@import './../common/abstracts/variable.scss';
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(tour) {
|
||||
@include e(popover) {
|
||||
background: $-dark-background2;
|
||||
}
|
||||
|
||||
@include e(info) {
|
||||
background: $-dark-background2;
|
||||
border-color: $-dark-background2;
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(skip) {
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(highlight) {
|
||||
|
||||
@include m(mask) {
|
||||
box-shadow: 0 0 0 100vh rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@include b(tour) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: $-tour-z-index;
|
||||
|
||||
@include e(mask) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@include e(highlight) {
|
||||
position: fixed;
|
||||
background: transparent;
|
||||
box-sizing: content-box;
|
||||
animation: tour-show $-tour-animation-duration $-tour-highlight-animation-timing;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
|
||||
@include m(mask) {
|
||||
box-shadow: 0 0 0 100vh $-tour-highlight-shadow-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(popover) {
|
||||
z-index: $-tour-popover-z-index;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: $-tour-popover-max-width;
|
||||
text-align: center;
|
||||
transition: $-tour-animation-duration all;
|
||||
background-color: $-tour-popover-bg;
|
||||
padding: $-tour-popover-padding;
|
||||
border-radius: $-tour-popover-radius;
|
||||
width: fit-content;
|
||||
min-width: $-tour-popover-min-width;
|
||||
}
|
||||
|
||||
@include e(info) {
|
||||
font-size: $-tour-popover-info-font-size;
|
||||
background: $-tour-info-bg;
|
||||
border: $-tour-info-border-width solid $-tour-info-border-color;
|
||||
color: $-tour-info-text-color;
|
||||
width: fit-content;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@include e(buttons) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 6px;
|
||||
color: $-tour-button-text-color;
|
||||
}
|
||||
|
||||
@include e(prev) {
|
||||
@include edeep(default) {
|
||||
font-size: $-tour-button-font-size;
|
||||
border-radius: $-tour-button-radius;
|
||||
padding: $-tour-button-padding;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(next) {
|
||||
@include edeep(default) {
|
||||
font-size: $-tour-button-font-size;
|
||||
border-radius: $-tour-button-radius;
|
||||
padding: $-tour-button-padding;
|
||||
background: $-tour-primary-button-bg-color;
|
||||
color: $-tour-primary-button-text-color;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(finish) {
|
||||
@include edeep(default) {
|
||||
font-size: $-tour-button-font-size;
|
||||
border-radius: $-tour-button-radius;
|
||||
padding: $-tour-button-padding;
|
||||
background: $-tour-primary-button-bg-color;
|
||||
color: $-tour-primary-button-text-color;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(skip) {
|
||||
|
||||
@include edeep(default) {
|
||||
font-size: $-tour-button-font-size;
|
||||
border-radius: $-tour-button-radius;
|
||||
padding: $-tour-button-padding;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tour-show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
238
node_modules/wot-design-uni/components/wd-tour/types.ts
generated
vendored
Normal file
238
node_modules/wot-design-uni/components/wd-tour/types.ts
generated
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp, makeArrayProp } from '../common/props'
|
||||
|
||||
export type MissingStrategy = 'skip' | 'stop' | 'hide'
|
||||
export interface TourStep {
|
||||
/**
|
||||
* 需要高亮的元素选择器
|
||||
*/
|
||||
element: string
|
||||
/**
|
||||
* 引导文字内容
|
||||
*/
|
||||
content: string
|
||||
/** 覆盖当前步骤的内边距 */
|
||||
padding?: number
|
||||
/** 覆盖当前步骤的提示与高亮间距 */
|
||||
offset?: number
|
||||
/** 强制提示位置 */
|
||||
placement?: 'auto' | 'top' | 'bottom' | 'left' | 'right'
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
maskColor: String,
|
||||
/**
|
||||
* 引导框与高亮元素之间的间距,单位 px
|
||||
* 类型:number
|
||||
* 默认值:20
|
||||
*/
|
||||
offset: makeNumberProp(20),
|
||||
|
||||
/**
|
||||
* 动画持续时间(毫秒)
|
||||
* 类型:number
|
||||
* 默认值:300
|
||||
*/
|
||||
duration: makeNumberProp(300),
|
||||
/**
|
||||
* 高亮区域的圆角大小
|
||||
* 类型:number
|
||||
* 默认值:8
|
||||
*/
|
||||
borderRadius: makeNumberProp(8),
|
||||
/**
|
||||
* 高亮区域的内边距
|
||||
* 类型:number
|
||||
* 默认值:8
|
||||
*/
|
||||
padding: makeNumberProp(8),
|
||||
/**
|
||||
* 上一步按钮文字
|
||||
*/
|
||||
prevText: String,
|
||||
|
||||
/**
|
||||
* 下一步按钮文字
|
||||
*/
|
||||
nextText: String,
|
||||
|
||||
/**
|
||||
* 跳过按钮文字
|
||||
*/
|
||||
skipText: String,
|
||||
|
||||
/**
|
||||
* 完成按钮文字
|
||||
*/
|
||||
finishText: String,
|
||||
|
||||
/**
|
||||
* 安全偏移量,用于滚动计算时确保元素周围有足够的空间
|
||||
* 类型: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<CSSProperties>,
|
||||
default: () => ({})
|
||||
},
|
||||
/**
|
||||
* 引导框的层级
|
||||
* 类型:number
|
||||
*/
|
||||
zIndex: Number,
|
||||
/**
|
||||
* 是否显示引导按钮
|
||||
* 类型:boolean
|
||||
* 默认值:true
|
||||
*/
|
||||
showTourButtons: makeBooleanProp(true),
|
||||
/** 查询作用域,限定选择器范围 */
|
||||
scope: {
|
||||
type: Object as PropType<any>
|
||||
},
|
||||
/**
|
||||
* 缺失元素处理策略
|
||||
* 类型:string
|
||||
* 可选值:'skip' | 'stop' | 'hide',分别表示跳过缺失元素、停止引导、隐藏缺失元素的提示
|
||||
* 默认值:'stop'
|
||||
*/
|
||||
missingStrategy: {
|
||||
type: String as PropType<MissingStrategy>,
|
||||
default: 'stop'
|
||||
}
|
||||
}
|
||||
|
||||
export type TourProps = typeof tourProps
|
||||
|
||||
export type TourChangeDetail = {
|
||||
/** 当前步骤的索引 */
|
||||
current: number
|
||||
}
|
||||
|
||||
export type TourSwitchDetail = {
|
||||
/** 上一步的索引 */
|
||||
prevCurrent: number
|
||||
/** 当前步骤的索引 */
|
||||
current: number
|
||||
/** 总步骤数 */
|
||||
total: number
|
||||
/** 目标元素是否在屏幕上半部分 */
|
||||
isElementInTop: boolean
|
||||
}
|
||||
|
||||
export type TourFinishDetail = {
|
||||
/** 当前步骤的索引 */
|
||||
current: number
|
||||
/** 总步骤数 */
|
||||
total: number
|
||||
}
|
||||
|
||||
export type TourErrorDetail = {
|
||||
/** 错误信息 */
|
||||
message: string
|
||||
/** 目标元素选择器 */
|
||||
element: string
|
||||
}
|
||||
|
||||
export type TourEmits = {
|
||||
/**
|
||||
* 更新 modelValue 事件,用于更新是否显示引导组件
|
||||
* @param value 是否显示引导组件
|
||||
*/
|
||||
'update:modelValue': [value: boolean]
|
||||
/**
|
||||
* 更新 current 事件,用于更新当前步骤索引
|
||||
* @param value 当前步骤索引
|
||||
*/
|
||||
'update:current': [value: number]
|
||||
/**
|
||||
* 切换事件,用于切换到上一步或下一步
|
||||
* @param detail 切换事件参数
|
||||
*/
|
||||
change: [detail: TourChangeDetail]
|
||||
/**
|
||||
* 上一步事件,用于切换到上一步
|
||||
* @param detail 上一步事件参数
|
||||
*/
|
||||
prev: [detail: TourSwitchDetail]
|
||||
/**
|
||||
* 下一步事件,用于切换到下一步
|
||||
* @param detail 下一步事件参数
|
||||
*/
|
||||
next: [detail: TourSwitchDetail]
|
||||
/**
|
||||
* 完成事件,用于完成引导
|
||||
* @param detail 完成事件参数
|
||||
*/
|
||||
finish: [detail: TourFinishDetail]
|
||||
/**
|
||||
* 跳过事件,用于跳过引导
|
||||
* @param detail 跳过事件参数
|
||||
*/
|
||||
skip: [detail: TourFinishDetail]
|
||||
/**
|
||||
* 错误事件,用于处理引导过程中出现的错误
|
||||
* @param detail 错误事件参数
|
||||
*/
|
||||
error: [detail: TourErrorDetail]
|
||||
}
|
||||
451
node_modules/wot-design-uni/components/wd-tour/wd-tour.vue
generated
vendored
Normal file
451
node_modules/wot-design-uni/components/wd-tour/wd-tour.vue
generated
vendored
Normal file
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<view class="wd-tour" v-if="modelValue" :style="rootStyle" @touchmove.stop.prevent="noop">
|
||||
<view class="wd-tour__mask" @click.stop="handleMask">
|
||||
<slot name="highlight" :elementInfo="highlightElementInfo">
|
||||
<view :class="highlightClass" :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, type CSSProperties } from 'vue'
|
||||
import { addUnit, getRect, getSystemInfo, isDef, objToStyle } from '../common/util'
|
||||
import { tourProps, type TourEmits } from './types'
|
||||
import { useRaf } from '../composables/useRaf'
|
||||
import { useTranslate } from '../composables/useTranslate'
|
||||
|
||||
const props = defineProps(tourProps)
|
||||
const emit = defineEmits<TourEmits>()
|
||||
const { translate } = useTranslate('tour')
|
||||
|
||||
const prevText = computed(() => {
|
||||
return isDef(props.prevText) ? props.prevText : translate('prev')
|
||||
})
|
||||
|
||||
const nextText = computed(() => {
|
||||
return isDef(props.nextText) ? props.nextText : translate('next')
|
||||
})
|
||||
|
||||
const skipText = computed(() => {
|
||||
return isDef(props.skipText) ? props.skipText : translate('skip')
|
||||
})
|
||||
|
||||
const finishText = computed(() => {
|
||||
return isDef(props.finishText) ? props.finishText : translate('finish')
|
||||
})
|
||||
|
||||
const currentIndex = ref<number>(0) // 当前步骤索引
|
||||
const elementInfo = ref<UniApp.NodeInfo>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
}) // 元素信息
|
||||
const windowHeight = ref<number>(0) // 窗口高度
|
||||
const windowTop = ref<number>(0) // 窗口顶部位置
|
||||
const isElementInTop = ref<boolean>(true) // 判断元素位置,确定提示信息在该元素的上方还是下方,true为上方,false为下方
|
||||
const lastScrollTop = ref<number>(0) //记录上一次滚动位置
|
||||
const statusBarHeight = ref<number>(0) // 状态栏高度
|
||||
const menuButtonInfo = ref(null as UniNamespace.GetMenuButtonBoundingClientRectRes | null)
|
||||
const topOffset = ref<number>(0) // 顶部偏移量
|
||||
|
||||
const rootStyle = computed(() => {
|
||||
const style: CSSProperties = {}
|
||||
if (isDef(props.zIndex)) {
|
||||
style.zIndex = props.zIndex
|
||||
}
|
||||
return objToStyle(style)
|
||||
})
|
||||
|
||||
const highlightClass = computed(() => {
|
||||
return `wd-tour__highlight ${props.mask ? 'wd-tour__highlight--mask' : ''}`
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const currentStep = computed(() => {
|
||||
return props.steps[currentIndex.value] || {}
|
||||
})
|
||||
// 提取公共的默认样式函数
|
||||
function getDefaultStyle(): CSSProperties {
|
||||
return {
|
||||
transition: props.duration + 'ms all'
|
||||
}
|
||||
}
|
||||
// 提取公共的高亮样式计算函数
|
||||
function calculateHighlightStyle(padding: number): CSSProperties {
|
||||
return {
|
||||
transition: props.duration + 'ms all,boxShadow 0s,height 0s,width 0s',
|
||||
borderRadius: props.borderRadius + 'px',
|
||||
padding: padding + 'px'
|
||||
}
|
||||
}
|
||||
const highlightStyle = computed(() => {
|
||||
// 如果元素信息尚未获取到,返回空样式避免闪烁
|
||||
if (!elementInfo.value.width && !elementInfo.value.height) {
|
||||
return getDefaultStyle()
|
||||
}
|
||||
const stepPadding = Number(isDef(currentStep.value.padding) ? currentStep.value.padding : props.padding)
|
||||
const baseStyle = calculateHighlightStyle(stepPadding)
|
||||
const style: CSSProperties = {
|
||||
...baseStyle,
|
||||
top: addUnit((elementInfo.value.top || 0) - stepPadding),
|
||||
left: addUnit((elementInfo.value.left || 0) - stepPadding),
|
||||
height: addUnit(elementInfo.value.height || 0),
|
||||
width: addUnit(elementInfo.value.width || 0)
|
||||
}
|
||||
if (isDef(props.mask) && isDef(props.maskColor)) {
|
||||
style.boxShadow = `0 0 0 100vh ${props.maskColor}`
|
||||
}
|
||||
return objToStyle([{ ...style }, props.highlightStyle])
|
||||
})
|
||||
|
||||
const popoverStyle = computed(() => {
|
||||
const style: CSSProperties = {}
|
||||
if (isDef(props.zIndex)) {
|
||||
const zIndex = Number(props.zIndex)
|
||||
style.zIndex = zIndex + 1
|
||||
style.transitionDuration = `${props.duration}ms`
|
||||
}
|
||||
const stepPadding = Number(isDef(currentStep.value.offset) ? currentStep.value.offset : props.offset)
|
||||
const placement = isDef(currentStep.value.placement) ? currentStep.value.placement : 'auto'
|
||||
const down = placement === 'bottom' || (placement === 'auto' && isElementInTop.value)
|
||||
if (down) {
|
||||
// 提示在元素下方
|
||||
style.top = addUnit((elementInfo.value.top || 0) + (elementInfo.value.height || 0) + Number(stepPadding))
|
||||
} else {
|
||||
// 提示在元素上方
|
||||
style.bottom = addUnit(windowHeight.value + windowTop.value - (elementInfo.value.top || 0) + Number(stepPadding))
|
||||
}
|
||||
|
||||
return objToStyle(style)
|
||||
})
|
||||
|
||||
const highlightElementInfo = computed(() => {
|
||||
const stepPadding = Number(isDef(currentStep.value.padding) ? currentStep.value.padding : props.padding)
|
||||
// 如果元素信息尚未获取到,返回空样式避免闪烁
|
||||
if (!elementInfo.value.width && !elementInfo.value.height) {
|
||||
return getDefaultStyle()
|
||||
}
|
||||
|
||||
const baseStyle = calculateHighlightStyle(stepPadding)
|
||||
const style: CSSProperties = {
|
||||
...baseStyle,
|
||||
top: addUnit((elementInfo.value.top || 0) - stepPadding),
|
||||
left: addUnit((elementInfo.value.left || 0) - stepPadding),
|
||||
width: addUnit((elementInfo.value.width || 0) + stepPadding * 2),
|
||||
height: addUnit((elementInfo.value.height || 0) + stepPadding * 2)
|
||||
}
|
||||
if (isDef(props.mask) && isDef(props.maskColor)) {
|
||||
style.boxShadow = `0 0 0 100vh ${props.maskColor}`
|
||||
}
|
||||
return style
|
||||
})
|
||||
function noop() {}
|
||||
// 方法
|
||||
async function updateElementInfo() {
|
||||
updateSystemInfo()
|
||||
const element = currentStep.value.element
|
||||
if (!element) return
|
||||
try {
|
||||
const res = (await getRect(element, false, props.scope)) as UniApp.NodeInfo
|
||||
initializeElementInfo(res)
|
||||
const effectiveBoundaries = getEffectiveBoundaries()
|
||||
const scrollNeeds = checkScrollNeeds(res, effectiveBoundaries)
|
||||
handleScrolling(res, scrollNeeds, effectiveBoundaries)
|
||||
calculateTipPosition(res)
|
||||
} catch (error) {
|
||||
console.error('updateElementInfo error:', error)
|
||||
emit('error', {
|
||||
message: '无法找到指定的引导元素',
|
||||
element: element
|
||||
})
|
||||
if (props.missingStrategy === 'skip') {
|
||||
handleNext()
|
||||
} else if (props.missingStrategy === 'hide') {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新系统信息
|
||||
function updateSystemInfo() {
|
||||
const sysInfo = getSystemInfo()
|
||||
windowHeight.value = sysInfo.windowHeight
|
||||
windowTop.value = sysInfo.windowTop || 0
|
||||
statusBarHeight.value = sysInfo.statusBarHeight || 0
|
||||
}
|
||||
|
||||
// 初始化元素信息
|
||||
function initializeElementInfo(res: UniApp.NodeInfo) {
|
||||
elementInfo.value = res
|
||||
// 调整元素位置信息,加上窗口顶部偏移量
|
||||
elementInfo.value.top = (res.top || 0) + windowTop.value
|
||||
elementInfo.value.bottom = ((res.bottom !== undefined ? res.bottom : (res.top || 0) + (res.height || 0)) as number) + windowTop.value
|
||||
}
|
||||
// 获取有效的页面边界(顶部和底部安全区域)
|
||||
function getEffectiveBoundaries() {
|
||||
// 有效顶部边界初始化为窗口顶部 + 顶部偏移量
|
||||
let effectiveWindowTop = windowTop.value + Number(topOffset.value)
|
||||
// 有效底部边界为窗口高度
|
||||
let effectiveWindowBottom = windowHeight.value
|
||||
return {
|
||||
top: effectiveWindowTop,
|
||||
bottom: effectiveWindowBottom
|
||||
}
|
||||
}
|
||||
// 检查是否需要滚动
|
||||
function checkScrollNeeds(res: UniApp.NodeInfo, boundaries: { top: number; bottom: number }) {
|
||||
// 判断元素是否被顶部遮挡(需要向上滚动)
|
||||
const needScrollUp = Number(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: UniApp.NodeInfo, scrollNeeds: { up: boolean; down: boolean }, boundaries: { top: number; bottom: number }) {
|
||||
if (scrollNeeds.up) {
|
||||
// 元素被顶部遮挡,需要提示框往上走,页面往下走
|
||||
scrollUp(res, boundaries)
|
||||
} else if (scrollNeeds.down) {
|
||||
// 元素被底部遮挡,需要提示框向下走,页面向上走
|
||||
scrollDown(res)
|
||||
}
|
||||
}
|
||||
|
||||
// 向引导上滚动处理
|
||||
function scrollUp(res: UniApp.NodeInfo, boundaries: { top: number; bottom: number }) {
|
||||
// 计算需要滚动的距离
|
||||
let scrollDistance = lastScrollTop.value + Number(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: () => {
|
||||
// 更新已滚动距离
|
||||
lastScrollTop.value = scrollDistance
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 引导向下滚动处理
|
||||
function scrollDown(res: UniApp.NodeInfo) {
|
||||
// 计算需要滚动的距离
|
||||
const bottom = res.bottom || 0
|
||||
let scrollDistance = bottom - windowHeight.value + props.padding + Number(props.bottomSafetyOffset)
|
||||
|
||||
// 更新元素位置信息(滚动后)
|
||||
elementInfo.value.top = windowHeight.value - bottom - props.padding - Number(props.bottomSafetyOffset) // 应该是减去安全偏移量
|
||||
elementInfo.value.bottom = windowHeight.value - props.padding - Number(props.bottomSafetyOffset)
|
||||
|
||||
uni.pageScrollTo({
|
||||
scrollTop: scrollDistance + lastScrollTop.value,
|
||||
duration: Number(props.duration),
|
||||
success: () => {
|
||||
// 更新已滚动距离
|
||||
lastScrollTop.value = scrollDistance + lastScrollTop.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算提示框显示位置(上方或下方)
|
||||
function calculateTipPosition(res: UniApp.NodeInfo) {
|
||||
// 计算导航区域总高度
|
||||
let totalNavHeight = statusBarHeight.value
|
||||
// 计算屏幕中心点位置
|
||||
const screenCenter = (windowHeight.value + totalNavHeight) / 2 + windowTop.value
|
||||
|
||||
// 计算元素中心点位置
|
||||
const elementCenter = (res.top || 0) + (res.height || 0) / 2 + windowTop.value
|
||||
|
||||
// 根据元素位置决定提示框显示在上方还是下方
|
||||
if (elementCenter < screenCenter) {
|
||||
isElementInTop.value = true
|
||||
} else {
|
||||
isElementInTop.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
if (currentIndex.value > 0) {
|
||||
const oldIndex = currentIndex.value
|
||||
currentIndex.value--
|
||||
emit('prev', {
|
||||
prevCurrent: oldIndex,
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length,
|
||||
isElementInTop: isElementInTop.value
|
||||
})
|
||||
emit('change', { current: currentIndex.value })
|
||||
}
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (currentIndex.value < props.steps.length - 1) {
|
||||
const oldIndex = currentIndex.value
|
||||
currentIndex.value++
|
||||
emit('next', {
|
||||
prevCurrent: oldIndex,
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length,
|
||||
isElementInTop: isElementInTop.value
|
||||
})
|
||||
emit('change', { current: currentIndex.value })
|
||||
} else {
|
||||
handleFinish()
|
||||
}
|
||||
}
|
||||
|
||||
function handleFinish() {
|
||||
emit('finish', {
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length
|
||||
})
|
||||
currentIndex.value = 0
|
||||
lastScrollTop.value = 0 // 重置滚动位置
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function handleSkip() {
|
||||
emit('skip', {
|
||||
current: currentIndex.value,
|
||||
total: props.steps.length
|
||||
})
|
||||
currentIndex.value = 0
|
||||
lastScrollTop.value = 0 // 重置滚动位置
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
function handleMask() {
|
||||
if (props.clickMaskNext) {
|
||||
handleNext()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.current,
|
||||
(newVal) => {
|
||||
currentIndex.value = newVal
|
||||
}
|
||||
)
|
||||
// 监听 currentIndex 变化,同步到父组件
|
||||
watch(
|
||||
() => currentIndex.value,
|
||||
(newVal) => {
|
||||
const raf = useRaf(updateElementInfo)
|
||||
nextTick(() => {
|
||||
raf.start()
|
||||
})
|
||||
emit('update:current', newVal)
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 modelValue 变化,当组件显示时更新系统信息
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
// 组件显示时重置滚动位置并更新系统信息
|
||||
lastScrollTop.value = 0
|
||||
updateSystemInfo()
|
||||
const raf = useRaf(() => {
|
||||
updateElementInfo()
|
||||
emit('update:current', currentIndex.value)
|
||||
})
|
||||
nextTick(() => {
|
||||
raf.start()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 所有平台统一处理逻辑
|
||||
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>
|
||||
Reference in New Issue
Block a user