|
|
@@ -1,201 +1,240 @@
|
|
|
<template>
|
|
|
- <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]">
|
|
|
- <view class="nut-popover-wrapper" @click="openPopover" ref="popoverRef" :id="'popoverRef' + refRandomId"
|
|
|
- ><slot name="reference"></slot
|
|
|
- ></view>
|
|
|
-
|
|
|
- <nut-popup
|
|
|
- :popClass="`nut-popover-content nut-popover-content--${location}`"
|
|
|
- :style="getStyles"
|
|
|
- v-model:visible="showPopup"
|
|
|
- position=""
|
|
|
- transition="nut-popover"
|
|
|
- :overlay="overlay"
|
|
|
- :duration="duration"
|
|
|
- :overlayStyle="overlayStyle"
|
|
|
- :overlayClass="overlayClass"
|
|
|
- :closeOnClickOverlay="closeOnClickOverlay"
|
|
|
- >
|
|
|
- <view ref="popoverContentRef" :id="'popoverContentRef' + refRandomId" class="nut-popover-content-group">
|
|
|
- <view :class="popoverArrow" v-if="showArrow"> </view>
|
|
|
- <slot name="content"></slot>
|
|
|
+ <view :class="classes">
|
|
|
+ <view class="nut-tour-masked" v-show="showTour" @click="handleClickMask"></view>
|
|
|
+
|
|
|
+ <view v-for="(step, i) in steps" :key="i" style="height: 0">
|
|
|
+ <template v-if="i == active">
|
|
|
<view
|
|
|
- v-for="(item, index) in list"
|
|
|
- :key="index"
|
|
|
- :class="[
|
|
|
- item.className,
|
|
|
- item.disabled && 'nut-popover-menu-disabled',
|
|
|
- 'nut-popover-menu-item',
|
|
|
- 'nut-popover-menu-taroitem'
|
|
|
- ]"
|
|
|
- @click.stop="chooseItem(item, index)"
|
|
|
+ class="nut-tour-mask"
|
|
|
+ :class="[mask ? '' : 'nut-tour-mask-none']"
|
|
|
+ :style="maskStyle"
|
|
|
+ v-if="showTour"
|
|
|
+ id="nut-tour-popid"
|
|
|
+ ></view>
|
|
|
+ <nut-popover
|
|
|
+ v-model:visible="showPopup"
|
|
|
+ :location="step.location || location"
|
|
|
+ targetId="nut-tour-popid"
|
|
|
+ :bgColor="bgColor"
|
|
|
+ :theme="theme"
|
|
|
+ :close-on-click-outside="false"
|
|
|
+ :offset="step.popoverOffset || [0, 12]"
|
|
|
+ :arrowOffset="step.arrowOffset || 0"
|
|
|
>
|
|
|
- <slot v-if="item.icon">
|
|
|
- <nut-icon
|
|
|
- v-bind="$attrs"
|
|
|
- class="nut-popover-item-img"
|
|
|
- :classPrefix="iconPrefix"
|
|
|
- :name="item.icon"
|
|
|
- ></nut-icon
|
|
|
- ></slot>
|
|
|
- <view class="nut-popover-menu-item-name">{{ item.name }}</view>
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </nut-popup>
|
|
|
-
|
|
|
- <view class="nut-popover-content-bg" v-if="showPopup" @touchmove="clickAway" @click="clickAway"></view>
|
|
|
+ <template #content>
|
|
|
+ <slot>
|
|
|
+ <view class="nut-tour-content" v-if="type == 'step'">
|
|
|
+ <view class="nut-tour-content-top">
|
|
|
+ <view @click="close">
|
|
|
+ <Close class="nut-tour-content-top-close" />
|
|
|
+ </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">
|
|
|
+ <view class="nut-tour-content-bottom-operate-btn" @click="changeStep('prev')" v-if="active != 0">{{
|
|
|
+ prevStepTxt
|
|
|
+ }}</view>
|
|
|
+ <view
|
|
|
+ class="nut-tour-content-bottom-operate-btn active"
|
|
|
+ @click="close"
|
|
|
+ v-if="steps.length - 1 == active"
|
|
|
+ >{{ completeTxt }}</view
|
|
|
+ >
|
|
|
+ <view class="nut-tour-content-bottom-operate-btn active" @click="changeStep('next')" v-else>{{
|
|
|
+ nextStepTxt
|
|
|
+ }}</view>
|
|
|
+ </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>
|
|
|
+ </template>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</template>
|
|
|
<script lang="ts">
|
|
|
-import { onMounted, computed, watch, ref, PropType, toRefs, reactive, CSSProperties } from 'vue';
|
|
|
+import { computed, watch, ref, reactive, toRefs, PropType, nextTick, onMounted } from 'vue';
|
|
|
+import { PopoverLocation } from '../popover/type';
|
|
|
import { createComponent } from '@/packages/utils/create';
|
|
|
-const { componentName, create } = createComponent('popover');
|
|
|
import { useTaroRect } from '@/packages/utils/useTaroRect';
|
|
|
-import { isArray } from '@/packages/utils/util';
|
|
|
+import { Close } from '@nutui/icons-vue';
|
|
|
import Taro from '@tarojs/taro';
|
|
|
|
|
|
+interface StepOptions {
|
|
|
+ target: Element;
|
|
|
+ content: String;
|
|
|
+ location?: PopoverLocation;
|
|
|
+ popoverOffset?: number[];
|
|
|
+ arrowOffset?: number;
|
|
|
+}
|
|
|
+const { create } = createComponent('tour');
|
|
|
export default create({
|
|
|
- inheritAttrs: false,
|
|
|
- components: {},
|
|
|
+ components: {
|
|
|
+ Close
|
|
|
+ },
|
|
|
props: {
|
|
|
visible: { type: Boolean, default: false },
|
|
|
- list: { type: Array, default: [] },
|
|
|
- theme: { type: String as PropType<import('./type').PopoverTheme>, default: 'light' },
|
|
|
- location: { type: String as PropType<import('./type').PopoverLocation>, default: 'bottom' },
|
|
|
- offset: { type: Array, default: [0, 12] },
|
|
|
- customClass: { type: String, default: '' },
|
|
|
- showArrow: { type: Boolean, default: true },
|
|
|
- iconPrefix: { type: String, default: 'nut-icon' },
|
|
|
- duration: { type: [Number, String], default: 0.3 },
|
|
|
- overlay: { type: Boolean, default: false },
|
|
|
- overlayClass: { type: String, default: '' },
|
|
|
- overlayStyle: { type: Object as PropType<CSSProperties> },
|
|
|
- closeOnClickOverlay: { type: Boolean, default: true },
|
|
|
- closeOnClickAction: { type: Boolean, default: true },
|
|
|
- closeOnClickOutside: { type: Boolean, default: true }
|
|
|
+ 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
|
|
|
+ }
|
|
|
},
|
|
|
- emits: ['update', 'update:visible', 'close', 'choose', 'open'],
|
|
|
+ emits: ['update:visible', 'change', 'close'],
|
|
|
setup(props, { emit }) {
|
|
|
- const popoverRef = ref();
|
|
|
- const popoverContentRef = ref();
|
|
|
- const showPopup = ref(props.visible);
|
|
|
const state = reactive({
|
|
|
- rootWidth: 0,
|
|
|
- rootHeight: 0
|
|
|
+ showTour: props.visible,
|
|
|
+ showPopup: false,
|
|
|
+ active: 0
|
|
|
});
|
|
|
|
|
|
- const popoverArrow = computed(() => {
|
|
|
- const prefixCls = 'nut-popover-arrow';
|
|
|
- const loca = props.location;
|
|
|
- const direction = loca.split('-')[0];
|
|
|
- return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`;
|
|
|
+ const maskRect = ref<{
|
|
|
+ [props: string]: number;
|
|
|
+ }>({});
|
|
|
+
|
|
|
+ const classes = computed(() => {
|
|
|
+ const prefixCls = 'nut-tour';
|
|
|
+ return `${prefixCls}`;
|
|
|
});
|
|
|
- const getStyles = computed(() => {
|
|
|
- let cross = +state.rootHeight;
|
|
|
- let lengthways = +state.rootWidth;
|
|
|
- let { offset, location } = props;
|
|
|
- if (isArray(offset) && offset.length == 2) {
|
|
|
- cross += +offset[1];
|
|
|
- lengthways += +offset[1];
|
|
|
- }
|
|
|
- const direction = location.split('-')[0];
|
|
|
- const style: CSSProperties = {};
|
|
|
- const mapd: any = {
|
|
|
- top: 'bottom',
|
|
|
- bottom: 'top',
|
|
|
- left: 'right',
|
|
|
- right: 'left'
|
|
|
+
|
|
|
+ const maskStyle = computed(() => {
|
|
|
+ const { offset, maskWidth, maskHeight } = props;
|
|
|
+ const { width, height, left, top } = maskRect.value;
|
|
|
+
|
|
|
+ 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`
|
|
|
};
|
|
|
- if (['top', 'bottom'].includes(direction)) {
|
|
|
- style[mapd[direction]] = `${cross}px`;
|
|
|
- style.marginLeft = `${offset[0]}px`;
|
|
|
- } else {
|
|
|
- style[mapd[direction]] = `${lengthways}px`;
|
|
|
- style.marginTop = `${offset[0]}px`;
|
|
|
- }
|
|
|
- return style;
|
|
|
+ return styles;
|
|
|
});
|
|
|
- // 获取宽度
|
|
|
- const getContentWidth = async () => {
|
|
|
- const refe = await useTaroRect(popoverRef, Taro);
|
|
|
- const { height, width } = refe;
|
|
|
- state.rootHeight = height;
|
|
|
- state.rootWidth = width;
|
|
|
- };
|
|
|
- watch(
|
|
|
- () => props.visible,
|
|
|
- (value) => {
|
|
|
- showPopup.value = value;
|
|
|
- if (value) {
|
|
|
- setTimeout(() => {
|
|
|
- getContentWidth();
|
|
|
- }, 200);
|
|
|
- }
|
|
|
+
|
|
|
+ const changeStep = (type: string) => {
|
|
|
+ if (type == 'next') {
|
|
|
+ state.active = state.active + 1;
|
|
|
+ } else {
|
|
|
+ state.active = state.active - 1;
|
|
|
}
|
|
|
- );
|
|
|
- const update = (val: boolean) => {
|
|
|
- emit('update', val);
|
|
|
- emit('update:visible', val);
|
|
|
+
|
|
|
+ state.showPopup = false;
|
|
|
+ nextTick(() => {
|
|
|
+ state.showPopup = true;
|
|
|
+ getRootPosition();
|
|
|
+ });
|
|
|
+
|
|
|
+ emit('change', state.active);
|
|
|
};
|
|
|
- const openPopover = () => {
|
|
|
- update(!props.visible);
|
|
|
- emit('open');
|
|
|
+
|
|
|
+ const getRootPosition = async () => {
|
|
|
+ const rect = await useTaroRect(props.steps[state.active].target, Taro);
|
|
|
+ maskRect.value = rect;
|
|
|
};
|
|
|
- const closePopover = () => {
|
|
|
+
|
|
|
+ const close = () => {
|
|
|
+ state.showTour = false;
|
|
|
+ state.showPopup = false;
|
|
|
+ emit('close', state.active);
|
|
|
emit('update:visible', false);
|
|
|
- emit('close');
|
|
|
};
|
|
|
- const chooseItem = (item: any, index: number) => {
|
|
|
- emit('choose', item, index);
|
|
|
- if (props.closeOnClickAction) {
|
|
|
- closePopover();
|
|
|
- }
|
|
|
- };
|
|
|
- const clickAway = (event: Event) => {
|
|
|
- closePopover();
|
|
|
+
|
|
|
+ const handleClickMask = () => {
|
|
|
+ props.closeOnClickOverlay && close();
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
setTimeout(() => {
|
|
|
- getContentWidth();
|
|
|
+ getRootPosition();
|
|
|
}, 200);
|
|
|
});
|
|
|
|
|
|
- const refRandomId = Math.random().toString(36).slice(-8);
|
|
|
+ watch(
|
|
|
+ () => props.visible,
|
|
|
+ (val) => {
|
|
|
+ if (val) {
|
|
|
+ state.active = 0;
|
|
|
+ getRootPosition();
|
|
|
+ }
|
|
|
+
|
|
|
+ state.showTour = val;
|
|
|
+ state.showPopup = val;
|
|
|
+ }
|
|
|
+ );
|
|
|
|
|
|
return {
|
|
|
- showPopup,
|
|
|
- openPopover,
|
|
|
- popoverArrow,
|
|
|
- closePopover,
|
|
|
- chooseItem,
|
|
|
- popoverRef,
|
|
|
- getStyles,
|
|
|
- popoverContentRef,
|
|
|
- refRandomId,
|
|
|
- clickAway
|
|
|
+ ...toRefs(state),
|
|
|
+ classes,
|
|
|
+ maskStyle,
|
|
|
+ changeStep,
|
|
|
+ close,
|
|
|
+ handleClickMask
|
|
|
};
|
|
|
}
|
|
|
});
|
|
|
</script>
|
|
|
-<style lang="scss">
|
|
|
-.self-content {
|
|
|
- width: 195px;
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- &-item {
|
|
|
- margin-top: 10px;
|
|
|
- margin-bottom: 10px;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- flex-direction: column;
|
|
|
- }
|
|
|
- &-desc {
|
|
|
- margin-top: 5px;
|
|
|
- width: 60px;
|
|
|
- font-size: 10px;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|