index.vue 6.3 KB

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