index.taro.vue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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 } 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. interface StepOptions {
  72. target: Element;
  73. content: String;
  74. location?: PopoverLocation;
  75. popoverOffset?: number[];
  76. arrowOffset?: number;
  77. }
  78. const { create } = createComponent('tour');
  79. export default create({
  80. components: {
  81. Close
  82. },
  83. props: {
  84. visible: { type: Boolean, default: false },
  85. type: {
  86. type: String,
  87. default: 'step' // tile
  88. },
  89. steps: {
  90. type: Array as PropType<StepOptions[]>,
  91. default: () => []
  92. },
  93. location: {
  94. type: String as PropType<PopoverLocation>,
  95. default: 'bottom'
  96. },
  97. current: {
  98. type: Number,
  99. default: 0
  100. },
  101. nextStepTxt: {
  102. type: String,
  103. default: '下一步'
  104. },
  105. prevStepTxt: {
  106. type: String,
  107. default: '上一步'
  108. },
  109. completeTxt: {
  110. type: String,
  111. default: '完成'
  112. },
  113. mask: {
  114. type: Boolean,
  115. default: true
  116. },
  117. offset: {
  118. type: Array as PropType<Number[]>,
  119. default: [8, 10]
  120. },
  121. bgColor: {
  122. type: String,
  123. default: ''
  124. },
  125. theme: {
  126. type: String,
  127. default: 'light'
  128. },
  129. maskWidth: {
  130. type: [Number, String],
  131. default: ''
  132. },
  133. maskHeight: {
  134. type: [Number, String],
  135. default: ''
  136. },
  137. closeOnClickOverlay: {
  138. type: Boolean,
  139. default: true
  140. }
  141. },
  142. emits: ['update:visible', 'change', 'close'],
  143. setup(props, { emit }) {
  144. const state = reactive({
  145. showTour: props.visible,
  146. showPopup: false,
  147. active: 0
  148. });
  149. const maskRect = ref<{
  150. [props: string]: number;
  151. }>({});
  152. const classes = computed(() => {
  153. const prefixCls = 'nut-tour';
  154. return `${prefixCls}`;
  155. });
  156. const maskStyle = computed(() => {
  157. const { offset, maskWidth, maskHeight } = props;
  158. const { width, height, left, top } = maskRect.value;
  159. const center = [left + width / 2, top + height / 2]; // 中心点 【横,纵】
  160. const w: number = Number(maskWidth ? maskWidth : width);
  161. const h: number = Number(maskHeight ? maskHeight : height);
  162. const styles = {
  163. width: `${w + +offset[1] * 2}px`,
  164. height: `${h + +offset[0] * 2}px`,
  165. top: `${center[1] - h / 2 - +offset[0]}px`,
  166. left: `${center[0] - w / 2 - +offset[1]}px`
  167. };
  168. return styles;
  169. });
  170. const changeStep = (type: string) => {
  171. if (type == 'next') {
  172. state.active = state.active + 1;
  173. } else {
  174. state.active = state.active - 1;
  175. }
  176. state.showPopup = false;
  177. nextTick(() => {
  178. state.showPopup = true;
  179. getRootPosition();
  180. });
  181. emit('change', state.active);
  182. };
  183. const getRootPosition = async () => {
  184. const rect = await useTaroRect(props.steps[state.active].target, Taro);
  185. maskRect.value = rect;
  186. };
  187. const close = () => {
  188. state.showTour = false;
  189. state.showPopup = false;
  190. emit('close', state.active);
  191. emit('update:visible', false);
  192. };
  193. const handleClickMask = () => {
  194. props.closeOnClickOverlay && close();
  195. };
  196. onMounted(() => {
  197. setTimeout(() => {
  198. getRootPosition();
  199. }, 200);
  200. });
  201. watch(
  202. () => props.visible,
  203. (val) => {
  204. if (val) {
  205. state.active = 0;
  206. getRootPosition();
  207. }
  208. state.showTour = val;
  209. state.showPopup = val;
  210. }
  211. );
  212. return {
  213. ...toRefs(state),
  214. classes,
  215. maskStyle,
  216. changeStep,
  217. close,
  218. handleClickMask
  219. };
  220. }
  221. });
  222. </script>