Files
cmgd-mini-app/uni_modules/nutui-uni/components/tour/tour.vue
2026-01-05 12:47:14 +08:00

224 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { ComponentInternalInstance } from 'vue'
import { computed, defineComponent, getCurrentInstance, onMounted, reactive, ref, watch } from 'vue'
import { CHANGE_EVENT, CLOSE_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { useRect } from '../_hooks'
import { getMainClass, getRandomId } from '../_utils'
import NutIcon from '../icon/icon.vue'
import NutPopover from '../popover/popover.vue'
import { tourEmits, tourProps } from './tour'
const props = defineProps(tourProps)
const emit = defineEmits(tourEmits)
const instance = getCurrentInstance() as ComponentInternalInstance
const refRandomId = getRandomId()
const state = reactive({
showTour: props.modelValue,
active: 0,
})
const showPopup = ref([false])
const maskRect: any[] = []
const maskStyles = ref<any[]>([])
const classes = computed(() => {
return getMainClass(props, componentName)
})
function maskStyle(index: number) {
const { offset, maskWidth, maskHeight } = props
if (!maskRect[index])
return {}
const { width, height, left, top } = maskRect[index]
const center = [left + width / 2, top + height / 2] // 中心点 【横,纵】
const w = Number(maskWidth || width)
const h = Number(maskHeight || height)
const styles = {
width: `${w + +offset[1] * 2}px`,
height: `${h + +offset[0] * 2}px`,
top: `${center[1] - h / 2 - +offset[0]}px`,
left: `${center[0] - w / 2 - +offset[1]}px`,
}
maskStyles.value[index] = styles
}
function changeStep(type: string) {
const current = state.active
let next = current
if (type === 'next')
next = current + 1
else
next = current - 1
showPopup.value[current] = false
setTimeout(() => {
showPopup.value[next] = true
state.active = next
}, 300)
emit(CHANGE_EVENT, state.active)
}
function getRootPosition() {
props.steps.forEach(async (item, i) => {
let rect
// #ifdef H5
rect = await useRect(item.target)
// #endif
// #ifndef H5
// TODO uniapp微信小程序无法实现获取不到组件外节点的信息
rect = await useRect(item.target, instance.root)
if (rect.left! < 0)
rect.left = -rect.left!
if (rect.top! < 0)
rect.top = -rect.top!
if (rect.right! < 0)
rect.right = -rect.right!
if (rect.bottom! < 0)
rect.bottom = -rect.bottom!
// #endif
maskRect[i] = rect
maskStyle(i)
})
}
function close() {
state.showTour = false
showPopup.value[state.active] = false
emit(CLOSE_EVENT, state.active)
emit(UPDATE_MODEL_EVENT, false)
}
function handleClickMask() {
props.closeOnClickOverlay && close()
}
onMounted(() => {
setTimeout(() => {
getRootPosition()
}, 500)
})
watch(
() => props.modelValue,
(val) => {
if (val) {
state.active = 0
getRootPosition()
}
state.showTour = val
showPopup.value[state.active] = val
},
)
</script>
<script lang="ts">
const componentName = `${PREFIX}-tour`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="customStyle">
<view v-if="state.showTour" class="nut-tour-masked" @click="handleClickMask" />
<view v-for="(step, i) in steps" :key="i" style="height: 0;">
<view
v-if="state.showTour"
:id="`nut-tour-popid${i}${refRandomId}`"
class="nut-tour-mask"
:class="[mask ? (showPopup[i] ? '' : 'nut-tour-mask-hidden') : 'nut-tour-mask-none']"
:style="maskStyles[i]"
/>
<NutPopover
v-if="state.showTour"
v-model:visible="showPopup[i]"
:location="step.location || location"
:target-id="`nut-tour-popid${i}${refRandomId}`"
:bg-color="bgColor"
:theme="theme"
:close-on-click-outside="false"
:offset="step.popoverOffset || [0, 12]"
:arrow-offset="step.arrowOffset || 0"
:duration="200"
>
<template #content>
<slot>
<view v-if="type === 'step'" class="nut-tour-content">
<view v-if="showTitleBar" class="nut-tour-content-top">
<view @click="close">
<NutIcon name="close" class="nut-tour-content-top-close" size="10px" />
</view>
</view>
<view class="nut-tour-content-inner">
{{ step.content }}
</view>
<view class="nut-tour-content-bottom">
<view class="nut-tour-content-bottom-init">
{{ state.active + 1 }}/{{ steps.length }}
</view>
<view class="nut-tour-content-bottom-operate">
<slot name="prev-step">
<view
v-if="state.active !== 0 && showPrevStep"
class="nut-tour-content-bottom-operate-btn"
@click="changeStep('prev')"
>
{{ prevStepTxt }}
</view>
</slot>
<view
v-if="steps.length - 1 === state.active"
class="nut-tour-content-bottom-operate-btn active"
@click="close"
>
{{ completeTxt }}
</view>
<slot name="next-step">
<view
v-if="steps.length - 1 !== state.active"
class="nut-tour-content-bottom-operate-btn active"
@click="changeStep('next')"
>
{{ nextStepTxt }}
</view>
</slot>
</view>
</view>
</view>
<view v-if="type === 'tile'" class="nut-tour-content nut-tour-content-tile">
<view class="nut-tour-content-inner">
{{ step.content }}
</view>
</view>
</slot>
</template>
</NutPopover>
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>