| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- <template>
- <view :class="classes">
- <view class="nut-tour-masked" v-if="showTour" @click="handleClickMask"></view>
- <view v-for="(step, i) in steps" :key="i" style="height: 0">
- <view
- class="nut-tour-mask"
- :class="[mask ? (showPopup[i] ? '' : 'nut-tour-mask-hidden') : 'nut-tour-mask-none']"
- :style="maskStyles[i]"
- :id="`nut-tour-popid${i}${refRandomId}`"
- ></view>
- <nut-popover
- v-model:visible="showPopup[i]"
- :location="step.location || location"
- :targetId="`nut-tour-popid${i}${refRandomId}`"
- :bgColor="bgColor"
- :theme="theme"
- :close-on-click-outside="false"
- :offset="step.popoverOffset || [0, 12]"
- :arrowOffset="step.arrowOffset || 0"
- :duration="0.2"
- >
- <template v-slot:content>
- <slot>
- <view class="nut-tour-content" v-if="type == 'step'">
- <view class="nut-tour-content-top" v-if="showTitleBar">
- <view @click="close">
- <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">{{ active + 1 }}/{{ steps.length }}</view>
- <view class="nut-tour-content-bottom-operate">
- <slot name="prev-step">
- <view
- class="nut-tour-content-bottom-operate-btn"
- @click="changeStep('prev')"
- v-if="active != 0 && showPrevStep"
- >{{ prevStepTxt }}</view
- >
- </slot>
- <view
- class="nut-tour-content-bottom-operate-btn active"
- @click="close"
- v-if="steps.length - 1 == active"
- >{{ completeTxt }}</view
- >
- <slot name="next-step">
- <view
- class="nut-tour-content-bottom-operate-btn active"
- @click="changeStep('next')"
- v-if="steps.length - 1 != active"
- >{{ nextStepTxt }}</view
- >
- </slot>
- </view>
- </view>
- </view>
- <view class="nut-tour-content nut-tour-content-tile" v-if="type == 'tile'">
- <view class="nut-tour-content-inner">
- {{ step.content }}
- </view>
- </view>
- </slot>
- </template>
- </nut-popover>
- </view>
- </view>
- </template>
- <script lang="ts">
- import { computed, watch, ref, reactive, toRefs, PropType, onMounted, Component, CSSProperties } from 'vue';
- import { PopoverLocation } from '../popover/type';
- import { createComponent } from '@/packages/utils/create';
- import { useTaroRect, rectTaro } from '@/packages/utils/useTaroRect';
- import { useRect } from '@/packages/utils/useRect';
- import { Close } from '@nutui/icons-vue-taro';
- import Taro from '@tarojs/taro';
- import Popover from '../popover/index.taro.vue';
- interface StepOptions {
- target: Element | string;
- content: string;
- location?: string;
- popoverOffset?: number[];
- arrowOffset?: number;
- }
- const { create } = createComponent('tour');
- export default create({
- components: {
- [Popover.name]: Popover as Component,
- Close
- },
- props: {
- modelValue: { type: Boolean, default: false },
- type: {
- type: String,
- default: 'step' // tile
- },
- steps: {
- type: Array as PropType<StepOptions[]>,
- default: () => []
- },
- location: {
- type: String as PropType<PopoverLocation>,
- default: 'bottom'
- },
- current: {
- type: Number,
- default: 0
- },
- nextStepTxt: {
- type: String,
- default: '下一步'
- },
- prevStepTxt: {
- type: String,
- default: '上一步'
- },
- completeTxt: {
- type: String,
- default: '完成'
- },
- mask: {
- type: Boolean,
- default: true
- },
- offset: {
- type: Array as PropType<Number[]>,
- default: [8, 10]
- },
- bgColor: {
- type: String,
- default: ''
- },
- theme: {
- type: String,
- default: 'light'
- },
- maskWidth: {
- type: [Number, String],
- default: ''
- },
- maskHeight: {
- type: [Number, String],
- default: ''
- },
- closeOnClickOverlay: {
- type: Boolean,
- default: true
- },
- showPrevStep: {
- type: Boolean,
- default: true
- },
- showTitleBar: {
- type: Boolean,
- default: true
- }
- },
- emits: ['update:modelValue', 'change', 'close'],
- setup(props, { emit }) {
- const state = reactive({
- showTour: props.modelValue,
- active: 0
- });
- const showPopup = ref([false]);
- let maskRect: rectTaro[] = [];
- let maskStyles = ref<any[]>([]);
- const classes = computed(() => {
- const prefixCls = 'nut-tour';
- return `${prefixCls}`;
- });
- const 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 = Number(maskWidth ? maskWidth : width);
- const h: number = Number(maskHeight ? 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;
- };
- const 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', state.active);
- };
- const getRootPosition = () => {
- props.steps.forEach(async (item, i) => {
- let rect;
- if (Taro.getEnv() === 'WEB') {
- const el = document.querySelector(`#${item.target}`) as Element;
- rect = await useRect(el);
- } else {
- rect = await useTaroRect(item.target, Taro);
- }
- console.log('获取taro', rect);
- maskRect[i] = rect;
- maskStyle(i);
- });
- };
- const close = () => {
- state.showTour = false;
- showPopup.value[state.active] = false;
- emit('close', state.active);
- emit('update:modelValue', false);
- };
- const 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;
- }
- );
- const refRandomId = Math.random().toString(36).slice(-8);
- return {
- ...toRefs(state),
- classes,
- maskStyle,
- changeStep,
- showPopup,
- close,
- handleClickMask,
- maskStyles,
- refRandomId
- };
- }
- });
- </script>
|