224 lines
6.0 KiB
Vue
224 lines
6.0 KiB
Vue
<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>
|