index.taro.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <template>
  2. <view :class="classes">
  3. <view class="nut-tour-masked" v-show="showTour" @click="handleClickMask"></view>
  4. <view v-for="(step, i) in steps" :key="i" style="height: 0">
  5. <template v-if="i == active">
  6. <view
  7. class="nut-tour-mask"
  8. :class="[mask ? '' : 'nut-tour-mask-none']"
  9. :style="maskStyle"
  10. v-if="showTour"
  11. id="nut-tour-popid"
  12. ></view>
  13. <nut-popover
  14. v-model:visible="showPopup"
  15. :location="step.location || location"
  16. targetId="nut-tour-popid"
  17. :bgColor="bgColor"
  18. :theme="theme"
  19. :close-on-click-outside="false"
  20. :offset="step.popoverOffset || [0, 12]"
  21. :arrowOffset="step.arrowOffset || 0"
  22. >
  23. <template #content>
  24. <slot>
  25. <view class="nut-tour-content" v-if="type == 'step'">
  26. <view class="nut-tour-content-top">
  27. <view @click="close">
  28. <Close class="nut-tour-content-top-close" />
  29. </view>
  30. </view>
  31. <view class="nut-tour-content-inner">
  32. {{ step.content }}
  33. </view>
  34. <view class="nut-tour-content-bottom">
  35. <view class="nut-tour-content-bottom-init">{{ active + 1 }}/{{ steps.length }}</view>
  36. <view class="nut-tour-content-bottom-operate">
  37. <view class="nut-tour-content-bottom-operate-btn" @click="changeStep('prev')" v-if="active != 0">{{
  38. prevStepTxt
  39. }}</view>
  40. <view
  41. class="nut-tour-content-bottom-operate-btn active"
  42. @click="close"
  43. v-if="steps.length - 1 == active"
  44. >{{ completeTxt }}</view
  45. >
  46. <view class="nut-tour-content-bottom-operate-btn active" @click="changeStep('next')" v-else>{{
  47. nextStepTxt
  48. }}</view>
  49. </view>
  50. </view>
  51. </view>
  52. <view class="nut-tour-content nut-tour-content-tile" v-if="type == 'tile'">
  53. <view class="nut-tour-content-inner">
  54. {{ step.content }}
  55. </view>
  56. </view>
  57. </slot>
  58. </template>
  59. </nut-popover>
  60. </template>
  61. </view>
  62. </view>
  63. </template>
  64. <script lang="ts">
  65. import { computed, watch, ref, reactive, toRefs, PropType, nextTick, onMounted, Component } from 'vue';
  66. import { PopoverLocation } from '../popover/type';
  67. import { createComponent } from '@/packages/utils/create';
  68. import { useTaroRect } from '@/packages/utils/useTaroRect';
  69. import { Close } from '@nutui/icons-vue-taro';
  70. import Taro from '@tarojs/taro';
  71. import Popover from '../popover/index.taro.vue';
  72. interface StepOptions {
  73. target: Element;
  74. content: String;
  75. location?: PopoverLocation;
  76. popoverOffset?: number[];
  77. arrowOffset?: number;
  78. }
  79. const { create } = createComponent('tour');
  80. export default create({
  81. components: {
  82. [Popover.name]: Popover as Component,
  83. Close
  84. },
  85. props: {
  86. visible: { type: Boolean, default: false },
  87. type: {
  88. type: String,
  89. default: 'step' // tile
  90. },
  91. steps: {
  92. type: Array as PropType<StepOptions[]>,
  93. default: () => []
  94. },
  95. location: {
  96. type: String as PropType<PopoverLocation>,
  97. default: 'bottom'
  98. },
  99. current: {
  100. type: Number,
  101. default: 0
  102. },
  103. nextStepTxt: {
  104. type: String,
  105. default: '下一步'
  106. },
  107. prevStepTxt: {
  108. type: String,
  109. default: '上一步'
  110. },
  111. completeTxt: {
  112. type: String,
  113. default: '完成'
  114. },
  115. mask: {
  116. type: Boolean,
  117. default: true
  118. },
  119. offset: {
  120. type: Array as PropType<Number[]>,
  121. default: [8, 10]
  122. },
  123. bgColor: {
  124. type: String,
  125. default: ''
  126. },
  127. theme: {
  128. type: String,
  129. default: 'light'
  130. },
  131. maskWidth: {
  132. type: [Number, String],
  133. default: ''
  134. },
  135. maskHeight: {
  136. type: [Number, String],
  137. default: ''
  138. },
  139. closeOnClickOverlay: {
  140. type: Boolean,
  141. default: true
  142. }
  143. },
  144. emits: ['update:visible', 'change', 'close'],
  145. setup(props, { emit }) {
  146. const state = reactive({
  147. showTour: props.visible,
  148. showPopup: false,
  149. active: 0
  150. });
  151. const maskRect = ref<{
  152. [props: string]: number;
  153. }>({});
  154. const classes = computed(() => {
  155. const prefixCls = 'nut-tour';
  156. return `${prefixCls}`;
  157. });
  158. const maskStyle = computed(() => {
  159. const { offset, maskWidth, maskHeight } = props;
  160. const { width, height, left, top } = maskRect.value;
  161. const center = [left + width / 2, top + height / 2]; // 中心点 【横,纵】
  162. const w: number = Number(maskWidth ? maskWidth : width);
  163. const h: number = Number(maskHeight ? maskHeight : height);
  164. const styles = {
  165. width: `${w + +offset[1] * 2}px`,
  166. height: `${h + +offset[0] * 2}px`,
  167. top: `${center[1] - h / 2 - +offset[0]}px`,
  168. left: `${center[0] - w / 2 - +offset[1]}px`
  169. };
  170. return styles;
  171. });
  172. const changeStep = (type: string) => {
  173. if (type == 'next') {
  174. state.active = state.active + 1;
  175. } else {
  176. state.active = state.active - 1;
  177. }
  178. state.showPopup = false;
  179. nextTick(() => {
  180. state.showPopup = true;
  181. getRootPosition();
  182. });
  183. emit('change', state.active);
  184. };
  185. const getRootPosition = async () => {
  186. const rect = await useTaroRect(props.steps[state.active].target, Taro);
  187. maskRect.value = rect;
  188. };
  189. const close = () => {
  190. state.showTour = false;
  191. state.showPopup = false;
  192. emit('close', state.active);
  193. emit('update:visible', false);
  194. };
  195. const handleClickMask = () => {
  196. props.closeOnClickOverlay && close();
  197. };
  198. onMounted(() => {
  199. setTimeout(() => {
  200. getRootPosition();
  201. }, 200);
  202. });
  203. watch(
  204. () => props.visible,
  205. (val) => {
  206. if (val) {
  207. state.active = 0;
  208. getRootPosition();
  209. }
  210. state.showTour = val;
  211. state.showPopup = val;
  212. }
  213. );
  214. return {
  215. ...toRefs(state),
  216. classes,
  217. maskStyle,
  218. changeStep,
  219. close,
  220. handleClickMask
  221. };
  222. }
  223. });
  224. </script>