index.taro.vue 7.4 KB

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